Files
OmniRoute/tests/unit/chatcore-memory-skills-injection.test.ts
Bob.Hou d6f315018a fix(chat): continue after a server-owned tool on Chat Completions (#12867)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, e a suíte vitest:ui completa (2149) verde.

Sobre esta PR especificamente: rodei os **23 arquivos de teste** que ela toca sobre o tip final, depois do merge da base — **392/392**. A migration `174_server_tool_executions.sql` não colide (o tip está em 173, e você já a renumerou em `c35f0fd7`).

O dono foi consultado antes do merge, porque o loop está atrás da flag `SERVER_OWNED_TOOL_LOOP_ENABLED` mas o primeiro send não-streaming mudou de dono sem flag, e a verificação manual em combo com Memory continuava desmarcada. A condição dele foi: entra se os testes focados passarem aqui. Passaram.

O lock de passthrough (`fetchCalls.length === 1`) é a parte que mais me convenceu — o double-dispatch que um `if (stream)` em volta do send existente causaria é exatamente o tipo de regressão que não aparece em teste de comportamento, só em contagem de chamada.

**Três ajustes meus na sua branch:**

1. `tests/unit/chatcore-stream-error-result.test.ts` procurava `"const legResult = await runNonStreamingProviderLeg"`, mas o seu commit final `6077b9dd` passou a reatribuir `legResult` e trocou para `let`. O guard falhava na sua própria branch (confirmei que o arquivo e o `chatCore.ts` eram byte-idênticos ao head da PR, então não era efeito da leva). Passou a aceitar `const|let` — a intenção do guard é o try/catch em volta da chamada, não a palavra-chave.

2. `tests/integration/skills-pipeline.test.ts` foi de 1156 para 1338 linhas e estourou o `testCap` de 1200. Segui o mesmo caminho que você já tinha tomado em `a1d2d20d` para os testes unitários: extraí os três casos do server-owned tool loop para `tests/integration/server-owned-tool-loop-pipeline.test.ts` (259 linhas), com instância própria do harness. O glob `tests/integration/*.test.ts` pega o arquivo novo sem registro adicional. 3/3 verdes isolados.

3. O arquivo novo herdou cinco `any` do original — que só passavam por estarem congelados no `eslint-suppressions.json` sob o nome antigo. Tipei como `Record<string, unknown>`. E `tests/unit/non-streaming-finalization.test.ts` tinha dois argumentos não usados em `trackPendingRequest`, agora prefixados com `_`.

Nada disso toca produção nem enfraquece asserção.
2026-09-07 09:15:00 -03:00

604 lines
22 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Isolated DATA_DIR set BEFORE importing anything that touches the DB
// (injectMemoryAndSkills -> getMemorySettings / retrieveMemories / injectSkills).
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mem-skills-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const {
getSkillsProviderForFormat,
injectMemoryAndSkills,
sortToolsByName,
mergeInjectedFallbackOwnerNames,
} = await import("../../open-sse/handlers/chatCore/memorySkillsInjection.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
const core = await import("../../src/lib/db/core.ts");
const { skillRegistry } = await import("../../src/lib/skills/registry.ts");
function resetSkillsRegistry() {
skillRegistry["registeredSkills"].clear();
skillRegistry["versionCache"].clear();
skillRegistry.invalidateCache();
}
test.after(() => {
resetSkillsRegistry();
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
// ─── getSkillsProviderForFormat (pure switch) ────────────────────────────────
test("getSkillsProviderForFormat maps CLAUDE -> anthropic", () => {
assert.equal(getSkillsProviderForFormat(FORMATS.CLAUDE), "anthropic");
});
test("getSkillsProviderForFormat maps GEMINI -> google", () => {
assert.equal(getSkillsProviderForFormat(FORMATS.GEMINI), "google");
});
test("getSkillsProviderForFormat maps OPENAI and any unknown format -> openai (default)", () => {
assert.equal(getSkillsProviderForFormat(FORMATS.OPENAI), "openai");
// any other / unknown format falls through to the default branch
assert.equal(getSkillsProviderForFormat("removed-google-cli"), "openai");
assert.equal(getSkillsProviderForFormat("codex"), "openai");
assert.equal(getSkillsProviderForFormat("totally-unknown"), "openai");
assert.equal(getSkillsProviderForFormat(""), "openai");
});
test("sortToolsByName sorts tools deterministically by function name or top-level name", () => {
const unsorted = [
{ function: { name: "z_tool" } },
{ name: "a_tool" },
{ function: { name: "m_tool" } },
];
const sorted = sortToolsByName(unsorted);
assert.deepEqual(sorted, [
{ name: "a_tool" },
{ function: { name: "m_tool" } },
{ function: { name: "z_tool" } },
]);
});
// ─── injectMemoryAndSkills ───────────────────────────────────────────────────
test("injectMemoryAndSkills with memoryOwnerId=null skips both branches and returns the body unchanged", async () => {
// memoryOwnerId is null -> memorySettings stays null -> the memory guard is false
// (no getMemorySettings/retrieveMemories) AND the skills guard (memorySettings?.skillsEnabled)
// is false. The body is returned verbatim with memorySettings=null.
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello world" }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: null,
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: null,
});
assert.equal(result.memorySettings, null, "memorySettings is null when no owner is provided");
// body is returned as-is (same reference, no injection happened)
assert.equal(result.body, body);
assert.deepEqual(result.body.messages, [{ role: "user", content: "hello world" }]);
assert.equal("tools" in result.body, false, "no skills were injected");
});
test("injectMemoryAndSkills with an empty DB resolves settings, finds nothing to inject, returns body unchanged", async () => {
// memoryOwnerId is set -> getMemorySettings() resolves DB defaults (enabled, skillsEnabled).
// The body has NO `messages` array (only `input`), so shouldInjectMemory() returns false and
// the memory-retrieval branch is skipped. The skills branch runs injectSkills(), but the
// empty DB registry has no skills, so mergedTools.length == existingTools.length and the body
// is NOT cloned/mutated. This exercises the realistic "nothing to inject" path end-to-end.
const log = {
debug: (..._args: unknown[]) => {
/* swallow */
},
};
const body: Record<string, unknown> = {
model: "gpt-4o",
input: [{ role: "user", content: "no messages array here" }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-empty-db",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log,
});
// memorySettings was resolved (defaults) — it is a real object, not null.
assert.ok(result.memorySettings, "memorySettings resolved from DB defaults");
// PRD-2026-06-19: memory is now OFF by default (skills still default on).
assert.equal(result.memorySettings.enabled, false);
assert.equal(result.memorySettings.skillsEnabled, true);
// No skills in the empty registry -> body returned unchanged (same reference).
assert.equal(result.body, body);
assert.equal("tools" in result.body, false, "no skills injected from an empty registry");
});
test("injectMemoryAndSkills resolves cleanly for a CLAUDE-format body with no owner (provider-mapping path)", async () => {
// Characterizes the no-owner short-circuit for a non-OpenAI source format. Nothing is
// injected; the function just returns the body untouched and memorySettings=null.
const body: Record<string, unknown> = {
model: "claude-3-5-sonnet",
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: null,
provider: "claude",
effectiveModel: "claude-3-5-sonnet",
sourceFormat: FORMATS.CLAUDE,
targetFormat: FORMATS.CLAUDE,
backgroundReason: "background-task",
log: null,
});
assert.equal(result.memorySettings, null);
assert.equal(result.body, body);
});
test("injectMemoryAndSkills injects memory tools when memory is enabled", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts");
const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 });
invalidateMemorySettingsCache();
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
tools: [{ type: "function", function: { name: "some_client_tool", description: "x" } }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-mem-on",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
assert.equal(result.memorySettings?.enabled, true);
const toolNames = (result.body.tools as { function?: { name?: string }; name?: string }[]).map(
(tool) => tool.function?.name ?? tool.name
);
for (const memoryTool of MEMORY_BUILTIN_TOOL_NAMES) {
assert.ok(
toolNames.includes(memoryTool),
`expected ${memoryTool} to be injected into body.tools`
);
}
assert.ok(toolNames.includes("some_client_tool"), "client tools are preserved");
invalidateMemorySettingsCache();
});
test("injectMemoryAndSkills does not inject server memory tools for stream requests", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts");
const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 });
invalidateMemorySettingsCache();
const body: Record<string, unknown> = {
model: "gpt-4o",
stream: true,
messages: [{ role: "user", content: "hello" }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-stream",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
assert.equal(result.memorySettings?.enabled, true);
const tools =
(result.body.tools as { function?: { name?: string }; name?: string }[] | undefined) ?? [];
const toolNames = tools.map((tool) => tool.function?.name ?? tool.name);
for (const memoryTool of MEMORY_BUILTIN_TOOL_NAMES) {
assert.equal(
toolNames.includes(memoryTool),
false,
`expected ${memoryTool} to be absent for stream requests (client-side MCP path)`
);
}
invalidateMemorySettingsCache();
});
test("injectMemoryAndSkills does not inject memory tools when memory is disabled", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts");
const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts");
await updateSettings({ memoryEnabled: false });
invalidateMemorySettingsCache();
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-mem-off",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
const tools =
(result.body.tools as { function?: { name?: string }; name?: string }[] | undefined) ?? [];
const toolNames = tools.map((tool) => tool.function?.name ?? tool.name);
for (const memoryTool of MEMORY_BUILTIN_TOOL_NAMES) {
assert.equal(
toolNames.includes(memoryTool),
false,
`expected ${memoryTool} to be absent when memory is disabled`
);
}
invalidateMemorySettingsCache();
});
// ─── Task 3: owner-set provenance + stream gate RED tests ────────────────────
test("stream:true + skills enabled + registry has items → no custom skill tool injected, injectedCustomSkillNames=[]", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache: inv2 } = await import("../../src/lib/memory/settings.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000, skillsEnabled: true });
inv2();
resetSkillsRegistry();
await skillRegistry.register({
name: "test-skill",
version: "1.0.0",
description: "test skill for stream gate",
schema: { input: {}, output: {} },
handler: "test-handler",
enabled: true,
apiKeyId: "owner-stream-skills",
mode: "on",
});
const body: Record<string, unknown> = {
model: "gpt-4o",
stream: true,
messages: [{ role: "user", content: "hello" }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-stream-skills",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
const toolNames = (
(result.body.tools as { function?: { name?: string }; name?: string }[] | undefined) ?? []
).map((t) => t.function?.name ?? t.name);
const hasCustomSkill = toolNames.some(
(n) => typeof n === "string" && (n.includes("test-skill") || n.startsWith("omr_skill_"))
);
assert.equal(hasCustomSkill, false, "stream:true must not inject custom skill tools");
assert.deepEqual(
(result as Record<string, unknown>).injectedCustomSkillNames,
[],
"injectedCustomSkillNames must be empty for stream requests"
);
resetSkillsRegistry();
inv2();
});
test("memory actual injection → builtinToolNames equals exactly the newly added memory tool names", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache: inv3 } = await import("../../src/lib/memory/settings.ts");
const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 });
inv3();
resetSkillsRegistry();
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
tools: [{ type: "function", function: { name: "some_client_tool", description: "x" } }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-builtin-own",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
const builtinToolNames = (result as Record<string, unknown>).builtinToolNames as
string[] | undefined;
assert.ok(builtinToolNames, "builtinToolNames must be present in result");
const expectedNewMemoryNames = [...MEMORY_BUILTIN_TOOL_NAMES];
assert.deepEqual(
builtinToolNames.sort(),
expectedNewMemoryNames.sort(),
"builtinToolNames must equal exactly the newly added memory tool names"
);
resetSkillsRegistry();
inv3();
});
test("client already has memory_search → not injected, not in builtinToolNames", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache: inv4 } = await import("../../src/lib/memory/settings.ts");
const { MEMORY_SEARCH_TOOL_NAME } = await import("../../src/lib/skills/memoryBuiltins.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 });
inv4();
resetSkillsRegistry();
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
tools: [
{
type: "function",
function: { name: MEMORY_SEARCH_TOOL_NAME, description: "client memory" },
},
],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-client-mem",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
const toolNames = (
(result.body.tools as { function?: { name?: string }[] | undefined }) ?? []
).map((t: { function?: { name?: string } }) => t.function?.name);
const memorySearchCount = toolNames.filter((n) => n === MEMORY_SEARCH_TOOL_NAME).length;
assert.equal(memorySearchCount, 1, "only one memory_search (client's) must exist");
const builtinToolNames = (result as Record<string, unknown>).builtinToolNames as
string[] | undefined;
assert.ok(builtinToolNames, "builtinToolNames must be present");
assert.equal(
builtinToolNames.includes(MEMORY_SEARCH_TOOL_NAME),
false,
"client-owned memory_search must NOT be in builtinToolNames"
);
resetSkillsRegistry();
inv4();
});
test("custom skill client collision: client has same encoded skill name → not injected, not in injectedCustomSkillNames", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache: inv5 } = await import("../../src/lib/memory/settings.ts");
const { encodeSkillToolName } = await import("../../src/lib/skills/injection.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000, skillsEnabled: true });
inv5();
resetSkillsRegistry();
await skillRegistry.register({
name: "collision-skill",
version: "1.0.0",
description: "skill that collides",
schema: { input: {}, output: {} },
handler: "collision-handler",
enabled: true,
apiKeyId: "owner-collision",
mode: "on",
});
const encodedName = encodeSkillToolName("collision-skill", "1.0.0");
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
tools: [{ type: "function", function: { name: encodedName, description: "client collision" } }],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-collision",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
const toolNames = (
(result.body.tools as { function?: { name?: string }[] | undefined }) ?? []
).map((t: { function?: { name?: string } }) => t.function?.name);
const count = toolNames.filter((n) => n === encodedName).length;
assert.equal(count, 1, "only one instance of encoded name must exist (client's)");
const injectedCustomSkillNames = (result as Record<string, unknown>).injectedCustomSkillNames as
string[] | undefined;
assert.ok(injectedCustomSkillNames, "injectedCustomSkillNames must be present");
assert.equal(
injectedCustomSkillNames.includes(encodedName),
false,
"client-owned skill name must NOT be in injectedCustomSkillNames"
);
resetSkillsRegistry();
inv5();
});
test("web-search fallback: client has same tool name → not added to builtinToolNames", async () => {
const { updateSettings } = await import("../../src/lib/db/settings.ts");
const { invalidateMemorySettingsCache: inv6 } = await import("../../src/lib/memory/settings.ts");
const { OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } =
await import("../../open-sse/services/webSearchFallback.ts");
await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 });
inv6();
resetSkillsRegistry();
const body: Record<string, unknown> = {
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
tools: [
{
type: "function",
function: { name: OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME, description: "client search" },
},
],
};
const result = await injectMemoryAndSkills({
body,
memoryOwnerId: "owner-websearch",
provider: "openai",
effectiveModel: "gpt-4o",
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.OPENAI,
backgroundReason: null,
log: { debug: () => {} },
});
const builtinToolNames = (result as Record<string, unknown>).builtinToolNames as
string[] | undefined;
assert.ok(builtinToolNames, "builtinToolNames must be present");
assert.equal(
builtinToolNames.includes(OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME),
false,
"client-owned web search tool must NOT be in builtinToolNames"
);
resetSkillsRegistry();
inv6();
});
// ─── Fix Round 2: Defect 5 — mergeInjectedFallbackOwnerNames + provenance ───
test("mergeInjectedFallbackOwnerNames: adds name only when enabled=true, convertedToolCount>0, toolName non-null, and not already in client tools", () => {
const result = mergeInjectedFallbackOwnerNames({ builtinToolNames: ["memory_search"] }, [
{ enabled: true, toolName: "omniroute_web_search", convertedToolCount: 2 },
{ enabled: true, toolName: null, convertedToolCount: 1 },
{ enabled: false, toolName: "omniroute_web_fetch", convertedToolCount: 3 },
{ enabled: true, toolName: "omniroute_web_fetch", convertedToolCount: 0 },
]);
assert.deepEqual(result.builtinToolNames, ["memory_search", "omniroute_web_search"]);
});
test("mergeInjectedFallbackOwnerNames: does not mutate input injectionResult", () => {
const input = { builtinToolNames: ["original"] };
const plans = [{ enabled: true, toolName: "omniroute_web_search", convertedToolCount: 1 }];
const result = mergeInjectedFallbackOwnerNames(input, plans);
// input must be unchanged
assert.deepEqual(input.builtinToolNames, ["original"]);
// result is a new object
assert.notEqual(result, input);
assert.deepEqual(result.builtinToolNames, ["original", "omniroute_web_search"]);
});
test("mergeInjectedFallbackOwnerNames: skips name already present in pre-conversion client tools", () => {
const result = mergeInjectedFallbackOwnerNames({ builtinToolNames: ["omniroute_web_search"] }, [
{ enabled: true, toolName: "omniroute_web_search", convertedToolCount: 2 },
]);
// Must not duplicate — omniroute_web_search already present
assert.deepEqual(result.builtinToolNames, ["omniroute_web_search"]);
});
// ─── Fix Round 3: Defect 3 — pre-conversion collision guard ─────────────────
test("mergeInjectedFallbackOwnerNames: client has omniroute_web_search → not added to builtinToolNames even if enabled=true", () => {
// Scenario: client sends {type:"web_search"} plus function named omniroute_web_search.
// prepareWebSearchFallbackBody emits enabled=true, convertedToolCount=2 (from the
// builtin conversion) but the synthetic tool was NOT added because client already has it.
// mergeInjectedFallbackOwnerNames must check pre-conversion client names.
const result = mergeInjectedFallbackOwnerNames(
{ builtinToolNames: [] },
[{ enabled: true, toolName: "omniroute_web_search", convertedToolCount: 2 }],
["omniroute_web_search"]
);
// Must NOT add omniroute_web_search — client already owns it
assert.deepEqual(result.builtinToolNames, []);
});
test("mergeInjectedFallbackOwnerNames: client has omniroute_web_fetch → not added to builtinToolNames", () => {
const result = mergeInjectedFallbackOwnerNames(
{ builtinToolNames: [] },
[{ enabled: true, toolName: "omniroute_web_fetch", convertedToolCount: 1 }],
["omniroute_web_fetch"]
);
assert.deepEqual(result.builtinToolNames, []);
});
test("mergeInjectedFallbackOwnerNames: client does NOT have the fallback name → added to builtinToolNames", () => {
const result = mergeInjectedFallbackOwnerNames(
{ builtinToolNames: [] },
[{ enabled: true, toolName: "omniroute_web_search", convertedToolCount: 2 }],
["some_other_tool"]
);
assert.deepEqual(result.builtinToolNames, ["omniroute_web_search"]);
});
test("mergeInjectedFallbackOwnerNames: no preConversionClientToolNames provided → falls back to existing behavior", () => {
const result = mergeInjectedFallbackOwnerNames({ builtinToolNames: [] }, [
{ enabled: true, toolName: "omniroute_web_search", convertedToolCount: 2 },
]);
assert.deepEqual(result.builtinToolNames, ["omniroute_web_search"]);
});