Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/lib/inference/ollama/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ function isProxyNonInteractive(): boolean {
// at module-load time). isNonInteractive is exported from onboard.ts;
// fall back to the env var if onboard hasn't fully loaded yet.
try {
const onboardMod = require("./onboard");
const onboardMod = require("../../onboard");
if (typeof onboardMod.isNonInteractive === "function") {
return Boolean(onboardMod.isNonInteractive());
}
Expand All @@ -648,7 +648,7 @@ function isProxyAutoYes(): boolean {
// The interactive override prompt path still covers --yes-only invocations
// because non-interactive mode is the gate that matters here.
try {
const onboardMod = require("./onboard");
const onboardMod = require("../../onboard");
if (typeof onboardMod.isAutoYes === "function") {
return Boolean(onboardMod.isAutoYes());
}
Expand All @@ -662,7 +662,7 @@ async function promptProxyYesNo(question: string, defaultIsYes: boolean): Promis
// Prefer onboard's promptYesNoOrDefault so we get the same indicator
// formatting and non-interactive note. Lazy-require to avoid the cycle.
try {
const onboardMod = require("./onboard");
const onboardMod = require("../../onboard");
if (typeof onboardMod.promptYesNoOrDefault === "function") {
return Boolean(await onboardMod.promptYesNoOrDefault(question, null, defaultIsYes));
}
Expand Down
80 changes: 80 additions & 0 deletions test/ollama-tools-capability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const require = createRequire(import.meta.url);
const REPO_ROOT = path.join(import.meta.dirname, "..");
const LOCAL_INFERENCE_PATH = path.join(REPO_ROOT, "dist", "lib", "inference", "local.js");
const ONBOARD_OLLAMA_PROXY_PATH = path.join(REPO_ROOT, "dist", "lib", "inference", "ollama", "proxy.js");
const ONBOARD_PATH = path.join(REPO_ROOT, "dist", "lib", "onboard.js");

type CapturedCall = { argv: readonly string[]; opts?: Record<string, unknown> };

Expand Down Expand Up @@ -43,6 +44,16 @@ interface OnboardOllamaProxyModule {
) => Promise<{ ok: boolean; message?: string }>;
}

interface OnboardModuleExports {
isAutoYes?: () => boolean;
isNonInteractive?: () => boolean;
promptYesNoOrDefault?: (
question: string,
envVar: string | null,
defaultIsYes: boolean,
) => boolean | Promise<boolean>;
}

function loadLocalInference(): LocalInferenceModule {
return require(LOCAL_INFERENCE_PATH) as LocalInferenceModule;
}
Expand Down Expand Up @@ -211,6 +222,7 @@ interface ProxyTestHarness {
logs: string[];
errors: string[];
promptCalls: string[];
setOnboardExports: (exports: OnboardModuleExports) => void;
setProbeResult: (caps: OllamaCapabilities) => void;
setPromptReply: (reply: string) => void;
}
Expand All @@ -225,10 +237,38 @@ const SHARED = {
promptReply: "",
promptCalls: [] as string[],
installed: false,
onboardExports: {} as OnboardModuleExports,
originalProbe: undefined as undefined | LocalInferenceModule["probeOllamaModelCapabilities"],
originalPrompt: undefined as undefined | ((msg: string) => Promise<string>),
originalOnboardCacheEntry: undefined as undefined | NodeJS.Module,
};

function resetOnboardExports(): void {
for (const key of Object.keys(SHARED.onboardExports) as Array<keyof OnboardModuleExports>) {
delete SHARED.onboardExports[key];
}
}

function installOnboardModuleStub(): void {
require.cache[ONBOARD_PATH] = {
id: ONBOARD_PATH,
filename: ONBOARD_PATH,
loaded: true,
exports: SHARED.onboardExports,
children: [],
path: path.dirname(ONBOARD_PATH),
paths: [],
} as unknown as NodeJS.Module;
}

function restoreOnboardModuleStub(): void {
if (SHARED.originalOnboardCacheEntry) {
require.cache[ONBOARD_PATH] = SHARED.originalOnboardCacheEntry;
return;
}
delete require.cache[ONBOARD_PATH];
}

function installSharedStubs(): void {
if (SHARED.installed) return;
const localInference = loadLocalInference() as LocalInferenceModule & {
Expand All @@ -248,6 +288,7 @@ function installSharedStubs(): void {
SHARED.promptCalls.push(msg);
return SHARED.promptReply;
};
SHARED.originalOnboardCacheEntry = require.cache[ONBOARD_PATH];

SHARED.installed = true;
}
Expand All @@ -262,6 +303,8 @@ function loadProxyWithStubs(): ProxyTestHarness {
};
SHARED.promptReply = "";
SHARED.promptCalls.length = 0;
resetOnboardExports();
installOnboardModuleStub();

// Invalidate the cache and re-require so the proxy module's
// destructured `probeOllamaModelCapabilities` binding picks up the
Expand All @@ -282,6 +325,8 @@ function loadProxyWithStubs(): ProxyTestHarness {
const restore = () => {
logSpy.mockRestore();
errSpy.mockRestore();
restoreOnboardModuleStub();
delete require.cache[ONBOARD_OLLAMA_PROXY_PATH];
};
(loadProxyWithStubs as unknown as { _restore?: () => void })._restore = restore;

Expand All @@ -290,6 +335,9 @@ function loadProxyWithStubs(): ProxyTestHarness {
logs,
errors,
promptCalls: SHARED.promptCalls,
setOnboardExports: (exports) => {
Object.assign(SHARED.onboardExports, exports);
},
setProbeResult: (caps) => {
SHARED.scriptedCaps = caps;
},
Expand Down Expand Up @@ -367,6 +415,20 @@ describe("checkOllamaModelToolSupport", () => {
expect(h.errors.some((e) => e.includes("NEMOCLAW_OLLAMA_REQUIRE_TOOLS=0"))).toBe(true);
});

it("uses onboard non-interactive state when the env var is unset", async () => {
const h = loadProxyWithStubs();
h.setOnboardExports({ isNonInteractive: () => true });
h.setProbeResult({
source: "api",
capabilities: ["completion"],
supportsTools: false,
});
const out = await h.proxy.checkOllamaModelToolSupport("phi4");
expect(out.ok).toBe(false);
expect(h.errors.some((e) => e.includes("NEMOCLAW_OLLAMA_REQUIRE_TOOLS=0"))).toBe(true);
expect(h.promptCalls.length).toBe(0);
});

it("non-interactive + NEMOCLAW_OLLAMA_REQUIRE_TOOLS=0 → {ok:true} after stderr warning", async () => {
process.env.NEMOCLAW_NON_INTERACTIVE = "1";
process.env.NEMOCLAW_OLLAMA_REQUIRE_TOOLS = "0";
Expand Down Expand Up @@ -401,6 +463,24 @@ describe("checkOllamaModelToolSupport", () => {
expect(h.promptCalls.length).toBe(0);
});

it("uses onboard prompt helper when available", async () => {
const h = loadProxyWithStubs();
const promptYesNoOrDefault = vi.fn(async () => true);
h.setOnboardExports({
isNonInteractive: () => false,
promptYesNoOrDefault,
});
h.setProbeResult({
source: "api",
capabilities: ["completion"],
supportsTools: false,
});
const out = await h.proxy.checkOllamaModelToolSupport("phi4");
expect(out).toEqual({ ok: true });
expect(promptYesNoOrDefault).toHaveBeenCalledWith(" Use this model anyway?", null, false);
expect(h.promptCalls.length).toBe(0);
});

it("probe failed (capabilities unknown) → {ok:true} (graceful degradation)", async () => {
const h = loadProxyWithStubs();
h.setProbeResult({
Expand Down
Loading