test/transport.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
import { ROOT } from "../src/paths.js";
import { applyTransport, authorTransport, researchExcerptCap, researchProfile, chat, ollamaThink, sanitizeModelData, DEFAULT_EXCERPT_CHARS } from "../src/ai/ollama.js";
import { excerptResearch, RESEARCH_EXCERPT } from "../src/ai/research.js";
import { providerModels, setHostedForTest } from "../src/cloud.js";
/**
* Whichever model config/models.yaml currently gives the author role.
*
* These tests fake `/api/tags` so `resolveRole` believes the configured model
* is installed; hardcoding its name made them fail the day the config named a
* different one, which is a fact about the config and not about the transport
* they exist to exercise.
*/
async function configuredAuthorModel() {
const cfg = YAML.parse(await readFile(path.join(ROOT, "config", "models.yaml"), "utf8"));
return cfg.roles.author.model;
}
// These exercise the LOCAL Ollama transport, so they must run in local mode.
// Without this the result depends on whether this machine's admin toggle
// (config/hosted.json) happens to be flipped — green in CI, red on a box that
// has been switched to hosted.
setHostedForTest(false);
test("model data drops XML-illegal controls without flattening its shape", () => {
assert.deepEqual(
sanitizeModelData({ headline: "Scale \u0019 decides", items: ["keep\nthis", { body: "grid\u0004ready" }] }),
{ headline: "Scale decides", items: ["keep\nthis", { body: "gridready" }] },
);
});
test("model data drops markdown bold — the renderer draws text, not markdown", () => {
assert.equal(
sanitizeModelData("kept **90% after 1,100 hours** under heat"),
"kept 90% after 1,100 hours under heat",
);
assert.equal(
sanitizeModelData("a 2**3 gain"),
"a 2**3 gain",
);
});
/* -------------------------------------------------------- applyTransport */
test("applyTransport leaves a spec without overrides untouched", () => {
const spec = { role: "author", num_predict: 12000 };
assert.equal(applyTransport(spec, { type: "ollama" }), spec);
assert.equal(applyTransport(spec, { type: "openai-compatible" }), spec);
});
test("applyTransport merges the cloud override only onto the cloud backend", () => {
const spec = { num_predict: 12000, excerpt_chars: 80000, transports: { cloud: { num_predict: 24000, excerpt_chars: 240000 } } };
const cloud = applyTransport(spec, { type: "openai-compatible" });
assert.equal(cloud.num_predict, 24000);
assert.equal(cloud.excerpt_chars, 240000);
const local = applyTransport(spec, { type: "ollama" });
assert.equal(local.num_predict, 12000);
assert.equal(local.excerpt_chars, 80000);
});
test("applyTransport replaces nested blocks wholesale, not as patches", () => {
const spec = { research: { angle_max: 8, per_query_limit: 8 }, transports: { cloud: { research: { angle_max: 10 } } } };
const cloud = applyTransport(spec, { type: "openai-compatible" });
assert.equal(cloud.research.angle_max, 10);
assert.equal(cloud.research.per_query_limit, undefined); // replaced, not merged
});
test("the local override wins on the local backend when both are declared", () => {
const spec = { num_predict: 12000, transports: { local: { num_predict: 8000 }, cloud: { num_predict: 24000 } } };
assert.equal(applyTransport(spec, { type: "ollama" }).num_predict, 8000);
assert.equal(applyTransport(spec, { type: "openai-compatible" }).num_predict, 24000);
});
test("authorTransport resolves by explicit model name without touching routing", async () => {
// deepseek-v4-flash is a declared opencode-go model → cloud.
assert.equal(await authorTransport({ model: "deepseek-v4-flash" }), "cloud");
// A name on no provider's list (a local pull) → ollama.
assert.equal(await authorTransport({ model: "qwen3:4b-instruct" }), "ollama");
});
test("researchExcerptCap follows the transport of the model that will read the notes", async () => {
assert.equal(await researchExcerptCap({ model: "deepseek-v4-flash" }), 240_000);
assert.equal(await researchExcerptCap({ model: "qwen3:4b-instruct" }), DEFAULT_EXCERPT_CHARS);
});
test("researchProfile resolves the research role's transport-aware depth", async () => {
const local = await researchProfile();
// The committed research role is local-first → the local budgets.
assert.equal(local.angle_max, 8);
assert.equal(local.per_query_limit, 8);
assert.ok(local.papers_limit >= 6);
// A fabricated cloud spec deepens every stage.
const spec = {
research: { angle_max: 8, per_query_limit: 8, per_query_read: 4 },
transports: { cloud: { research: { angle_max: 10, per_query_limit: 12, per_query_read: 8 } } },
};
const cloud = applyTransport(spec, { type: "openai-compatible" }).research;
assert.equal(cloud.angle_max, 10);
assert.equal(cloud.per_query_limit, 12);
assert.equal(cloud.per_query_read, 8);
});
/* ------------------------------------------------- models.yaml contract */
test("models.yaml declares a per-transport split that is strictly deeper on cloud", async () => {
const cfg = YAML.parse(await readFile(path.join(ROOT, "config", "models.yaml"), "utf8"));
const author = cfg.roles.author;
const research = cfg.roles.research;
assert.ok(author.transports.cloud.num_predict >= author.num_predict * 2, "cloud author output cap must not inherit the local cap");
assert.ok(author.transports.cloud.excerpt_chars > author.excerpt_chars, "cloud author research budget must exceed the local one");
assert.equal(author.transports.cloud.excerpt_chars, 240_000);
const local = research.research;
const cloud = research.transports.cloud.research;
for (const key of Object.keys(local)) {
assert.ok(cloud[key] >= local[key], `cloud research ${key} (${cloud[key]}) must not be smaller than local (${local[key]})`);
}
assert.ok(cfg.defaults.num_predict_bump_ceiling >= 2 * author.num_predict, "the bump ceiling must give the cloud cap room to double");
});
/* ------------------------------------------------------- excerpt cap param */
test("excerptResearch honours an explicit cap", () => {
const text = `## A\n\n${"word ".repeat(RESEARCH_EXCERPT)}tail-line\n`;
const out = excerptResearch(text, 10_000);
assert.ok(out.length <= 10_000);
assert.ok(out.endsWith("\n"));
assert.ok(excerptResearch(text).length <= RESEARCH_EXCERPT);
});
/* ------------------------------------------------------- length auto-bump */
function fakeResponse(body) {
return {
ok: true,
status: 200,
json: async () => body,
text: async () => "",
};
}
/* ------------------------------------------------- hosted model curation */
test("providerModels returns the static list when one is declared", async () => {
assert.deepEqual(await providerModels({ models: ["a", "b"], baseURL: "https://x.example/v1" }), ["a", "b"]);
});
test("providerModels fetches GET {baseURL}/models when the list is empty and a key exists", async () => {
const orig = globalThis.fetch;
process.env.FAKE_PROV_KEY = "envkey";
globalThis.fetch = async (url, opts = {}) => {
assert.equal(String(url), "https://x.example/v1/models");
assert.equal(opts.headers.Authorization, "Bearer envkey");
assert.match(opts.headers["x-opencode-session"], /^[0-9a-f-]{36}$/);
return fakeResponse({ data: [{ id: "m1" }, { id: "m2" }] });
};
try {
const list = await providerModels({ models: [], baseURL: "https://x.example/v1/", apiKey: "env:FAKE_PROV_KEY", session_header: true });
assert.deepEqual(list, ["m1", "m2"]);
} finally {
globalThis.fetch = orig;
delete process.env.FAKE_PROV_KEY;
}
});
test("providerModels handles the string-array /models shape too", async () => {
const orig = globalThis.fetch;
process.env.FAKE_PROV_KEY = "envkey";
globalThis.fetch = async () => fakeResponse({ models: ["one", "two"] });
try {
const list = await providerModels({ baseURL: "https://y.example/v1", apiKey: "env:FAKE_PROV_KEY" });
assert.deepEqual(list, ["one", "two"]);
} finally {
globalThis.fetch = orig;
delete process.env.FAKE_PROV_KEY;
}
});
test("providerModels returns empty on a failed fetch, never throwing", async () => {
const orig = globalThis.fetch;
globalThis.fetch = async () => ({ ok: false, status: 401, json: async () => ({}), text: async () => "nope" });
try {
assert.deepEqual(await providerModels({ baseURL: "https://z.example/v1", apiKey: "env:FAKE_KEY" }), []);
} finally {
globalThis.fetch = orig;
}
});
test("chat() auto-bumps the cap when a non-streamed response is cut at length", async () => {
const orig = globalThis.fetch;
const caps = [];
const AUTHOR = await configuredAuthorModel();
globalThis.fetch = async (url, opts = {}) => {
const u = String(url);
if (u.endsWith("/api/tags")) {
return fakeResponse({ models: [{ name: AUTHOR }] });
}
if (u.endsWith("/api/chat")) {
const body = JSON.parse(opts.body ?? "{}");
caps.push(body.options?.num_predict);
const truncated = caps.length === 1;
return fakeResponse({
model: AUTHOR,
message: { content: '{"ops":[]}' },
done_reason: truncated ? "length" : "stop",
eval_count: 1234,
});
}
throw new Error(`unexpected URL ${u}`);
};
try {
// An explicit local model name skips the routing preference and forces the
// ollama backend, so the test is deterministic on any machine.
const res = await chat({ role: "author", model: AUTHOR, messages: [{ role: "user", content: "hi" }] });
assert.equal(caps.length, 2);
assert.equal(caps[0], 12000); // the local author cap
assert.equal(caps[1], 24000); // doubled by the auto-bump
assert.equal(res.doneReason, "stop");
assert.equal(res.cap, 24000);
assert.equal(res.transport, "ollama");
} finally {
globalThis.fetch = orig;
}
});
test("chat() leaves a streamed length-truncated response alone (tokens cannot be unsaid)", async () => {
const orig = globalThis.fetch;
const caps = [];
const AUTHOR = await configuredAuthorModel();
globalThis.fetch = async (url, opts = {}) => {
const u = String(url);
if (u.endsWith("/api/tags")) {
return fakeResponse({ models: [{ name: AUTHOR }] });
}
if (u.endsWith("/api/chat")) {
const body = JSON.parse(opts.body ?? "{}");
caps.push(body.options?.num_predict);
assert.equal(body.stream, true);
// NDJSON stream: one truncated line, done_reason=length.
const line = JSON.stringify({ message: { content: "partial" }, done: true, done_reason: "length" });
const reader = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(line + "\n"));
controller.close();
},
}).getReader();
return { ok: true, body: { getReader: () => reader } };
}
throw new Error(`unexpected URL ${u}`);
};
try {
const res = await chat({
role: "author", model: AUTHOR,
messages: [{ role: "user", content: "hi" }],
onToken: () => {},
});
assert.equal(caps.length, 1); // no bump for a streamed call
assert.equal(res.doneReason, "length");
} finally {
globalThis.fetch = orig;
}
});
/* ------------------------------------------------------------- thinking */
/**
* `roles.author.thinking` was declared and read on exactly one path: the
* openai-compatible one. The Ollama payload never carried it, so on the local
* backend the flag was a comment — the same shape as `vision` before
* `roleCanSeeImages`.
*/
test("a thinking role sends `think` at its level to a model that reports the capability", async () => {
const orig = globalThis.fetch;
const AUTHOR = await configuredAuthorModel();
let sent;
globalThis.fetch = async (url, opts = {}) => {
const u = String(url);
if (u.endsWith("/api/tags")) return fakeResponse({ models: [{ name: AUTHOR }] });
if (u.endsWith("/api/show")) return fakeResponse({ capabilities: ["completion", "vision", "thinking"] });
if (u.endsWith("/api/chat")) {
sent = JSON.parse(opts.body ?? "{}");
return fakeResponse({ model: AUTHOR, message: { content: "{}" }, done_reason: "stop" });
}
throw new Error(`unexpected URL ${u}`);
};
try {
await chat({ role: "author", model: AUTHOR, messages: [{ role: "user", content: "hi" }] });
// config/models.yaml puts the author at reasoning_effort: medium.
console.log("SENT1:", JSON.stringify({think: sent?.think, model: sent?.model}));
assert.equal(sent.think, "medium");
} finally {
globalThis.fetch = orig;
}
});
test("a role whose model cannot think sends nothing — an unsupported `think` is an error, not a no-op", async () => {
const orig = globalThis.fetch;
const PLAIN = "no-thoughts:1b";
let sent;
globalThis.fetch = async (url, opts = {}) => {
const u = String(url);
if (u.endsWith("/api/tags")) return fakeResponse({ models: [{ name: PLAIN }] });
if (u.endsWith("/api/show")) return fakeResponse({ capabilities: ["completion"] });
if (u.endsWith("/api/chat")) {
sent = JSON.parse(opts.body ?? "{}");
return fakeResponse({ model: PLAIN, message: { content: "{}" }, done_reason: "stop" });
}
throw new Error(`unexpected URL ${u}`);
};
try {
await chat({ role: "author", model: PLAIN, messages: [{ role: "user", content: "hi" }] });
assert.ok(!("think" in sent), `no think key at all, got ${JSON.stringify(sent.think)}`);
} finally {
globalThis.fetch = orig;
}
});
test("a role that does not declare thinking turns it OFF explicitly", async () => {
// The `research` role, which shares the author's model and declares no
// thinking — so the only difference under test is the role's own config.
const orig = globalThis.fetch;
const M = "research-capable:35b";
const UTILITY = await configuredAuthorModel();
let sent;
globalThis.fetch = async (url, opts = {}) => {
const u = String(url);
if (u.endsWith("/api/tags")) return fakeResponse({ models: [{ name: M }, { name: UTILITY }] });
if (u.endsWith("/api/show")) return fakeResponse({ capabilities: ["completion", "thinking"] });
if (u.endsWith("/api/chat")) {
sent = JSON.parse(opts.body ?? "{}");
return fakeResponse({ model: M, message: { content: "{}" }, done_reason: "stop" });
}
throw new Error(`unexpected URL ${u}`);
};
try {
await chat({ role: "research", model: M, messages: [{ role: "user", content: "hi" }] });
// Not "absent" — FALSE. The model's own default is to think, so omitting
// the flag meant "whatever the template does", and the research role was
// measured returning 16,948 characters of reasoning it never asked for.
// Sending false took that call from 27 seconds to 3.
assert.equal(sent.think, false, "the role's declaration is the gate, and it has to be sent");
} finally {
globalThis.fetch = orig;
}
});
test("xhigh is the gateway's vocabulary and maps to Ollama's top level", async () => {
const orig = globalThis.fetch;
globalThis.fetch = async (url) => {
if (String(url).endsWith("/api/show")) return fakeResponse({ capabilities: ["thinking"] });
throw new Error("unexpected");
};
try {
const at = (effort, model) =>
ollamaThink({ thinking: true, reasoning_effort: effort, model, backend: { baseURL: "http://t.example" } });
assert.equal(await at("xhigh", "m-xhigh"), "high", "Ollama has no xhigh; passing it through is a 400");
assert.equal(await at("low", "m-low"), "low");
assert.equal(await at(undefined, "m-none"), true, "declared without a level is still on");
assert.equal(
await ollamaThink({ thinking: false, model: "m-off", backend: { baseURL: "http://t.example" } }),
false,
"a capable model is told NOT to think, rather than left to its default",
);
} finally {
globalThis.fetch = orig;
}
});