test(auth): add comprehensive tests for Ollama provider
Test coverage for: - Ollama reachable with models discovered - Custom endpoint configuration - Empty models list (fallback model) - Existing OLLAMA_API_KEY usage and decline - Non-200 response handling - Malformed response handling - setDefaultModel=false (agent model override) - Provider mapping verification
This commit is contained in:
parent
48b5d4c857
commit
9660ba5786
@ -1,10 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { resolveImplicitProviders } from "./models-config.providers.js";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
describe("Ollama provider", () => {
|
||||
const previousOllamaKey = process.env.OLLAMA_API_KEY;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear OLLAMA_API_KEY to test the "no key" scenario
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original env var
|
||||
if (previousOllamaKey === undefined) {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
} else {
|
||||
process.env.OLLAMA_API_KEY = previousOllamaKey;
|
||||
}
|
||||
});
|
||||
|
||||
it("should not include ollama when no API key is configured", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-"));
|
||||
const providers = await resolveImplicitProviders({ agentDir });
|
||||
|
||||
@ -33,6 +33,7 @@ describe("applyAuthChoice", () => {
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const previousAiGatewayKey = process.env.AI_GATEWAY_API_KEY;
|
||||
const previousOllamaKey = process.env.OLLAMA_API_KEY;
|
||||
const previousSshTty = process.env.SSH_TTY;
|
||||
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
|
||||
let tempStateDir: string | null = null;
|
||||
@ -69,6 +70,11 @@ describe("applyAuthChoice", () => {
|
||||
} else {
|
||||
process.env.AI_GATEWAY_API_KEY = previousAiGatewayKey;
|
||||
}
|
||||
if (previousOllamaKey === undefined) {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
} else {
|
||||
process.env.OLLAMA_API_KEY = previousOllamaKey;
|
||||
}
|
||||
if (previousSshTty === undefined) {
|
||||
delete process.env.SSH_TTY;
|
||||
} else {
|
||||
@ -492,6 +498,549 @@ describe("applyAuthChoice", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("configures Ollama when selecting ollama and Ollama is reachable", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "http://127.0.0.1:11434/api/tags") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [
|
||||
{ name: "llama3.3:latest", modified_at: "2024-01-01", size: 1000, digest: "abc" },
|
||||
{ name: "qwen2.5:latest", modified_at: "2024-01-01", size: 2000, digest: "def" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const confirm = vi.fn(async () => false); // Don't use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("custom Ollama endpoint"),
|
||||
}),
|
||||
);
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toMatchObject({
|
||||
provider: "ollama",
|
||||
mode: "api_key",
|
||||
});
|
||||
// Should use first discovered model as default
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("ollama/llama3.3:latest");
|
||||
|
||||
const authProfilePath = authProfilePathFor(requireAgentDir());
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["ollama:default"]?.key).toBe("ollama");
|
||||
});
|
||||
|
||||
it("uses existing OLLAMA_API_KEY when selecting ollama", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
process.env.OLLAMA_API_KEY = "my-ollama-key";
|
||||
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "http://127.0.0.1:11434/api/tags") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [{ name: "llama3.3", modified_at: "2024-01-01", size: 1000, digest: "abc" }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const confirm = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(false) // Don't use custom endpoint
|
||||
.mockResolvedValueOnce(true); // Use existing OLLAMA_API_KEY
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("OLLAMA_API_KEY"),
|
||||
}),
|
||||
);
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toMatchObject({
|
||||
provider: "ollama",
|
||||
mode: "api_key",
|
||||
});
|
||||
|
||||
const authProfilePath = authProfilePathFor(requireAgentDir());
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["ollama:default"]?.key).toBe("my-ollama-key");
|
||||
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
});
|
||||
|
||||
it("shows error when Ollama is not reachable", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const fetchSpy = vi.fn(async () => {
|
||||
throw new Error("Connection refused");
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const note = vi.fn(noopAsync);
|
||||
const confirm = vi.fn(async () => false); // Don't use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note,
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Ollama is not reachable"),
|
||||
"Ollama not detected",
|
||||
);
|
||||
// Should not set auth profile when Ollama is not reachable
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses custom Ollama endpoint when specified", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const customEndpoint = "http://192.168.1.100:11434";
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === `${customEndpoint}/api/tags`) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [
|
||||
{ name: "mistral:latest", modified_at: "2024-01-01", size: 1000, digest: "abc" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const text = vi.fn(async () => customEndpoint);
|
||||
const confirm = vi.fn(async () => true); // Use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text,
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Enter Ollama endpoint URL" }),
|
||||
);
|
||||
expect(fetchSpy).toHaveBeenCalledWith(`${customEndpoint}/api/tags`, expect.anything());
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("ollama/mistral:latest");
|
||||
// Custom endpoint should be in provider config
|
||||
expect(result.config.models?.providers?.ollama).toMatchObject({
|
||||
baseUrl: `${customEndpoint}/v1`,
|
||||
api: "openai-completions",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses fallback model when Ollama returns empty models list", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "http://127.0.0.1:11434/api/tags") {
|
||||
return new Response(JSON.stringify({ models: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const note = vi.fn(noopAsync);
|
||||
const confirm = vi.fn(async () => false); // Don't use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note,
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(note).toHaveBeenCalledWith(expect.stringContaining("No models found"), "Ollama");
|
||||
// Should use the fallback model
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("ollama/llama3.3");
|
||||
});
|
||||
|
||||
it("uses placeholder when user declines existing OLLAMA_API_KEY", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
process.env.OLLAMA_API_KEY = "my-secret-ollama-key";
|
||||
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "http://127.0.0.1:11434/api/tags") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [{ name: "llama3.3", modified_at: "2024-01-01", size: 1000, digest: "abc" }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const confirm = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(false) // Don't use custom endpoint
|
||||
.mockResolvedValueOnce(false); // Decline existing OLLAMA_API_KEY
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
// Should have asked about existing key
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("OLLAMA_API_KEY"),
|
||||
}),
|
||||
);
|
||||
|
||||
const authProfilePath = authProfilePathFor(requireAgentDir());
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
// Should use placeholder "ollama" instead of the env key
|
||||
expect(parsed.profiles?.["ollama:default"]?.key).toBe("ollama");
|
||||
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
});
|
||||
|
||||
it("treats non-200 Ollama response as unreachable", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const fetchSpy = vi.fn(async () => {
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const note = vi.fn(noopAsync);
|
||||
const confirm = vi.fn(async () => false); // Don't use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note,
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Ollama is not reachable"),
|
||||
"Ollama not detected",
|
||||
);
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns agentModelOverride when setDefaultModel is false", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "http://127.0.0.1:11434/api/tags") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [
|
||||
{ name: "codellama:latest", modified_at: "2024-01-01", size: 1000, digest: "abc" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const confirm = vi.fn(async () => false); // Don't use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: false,
|
||||
});
|
||||
|
||||
// Should not change the existing default model
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5");
|
||||
// Should return the override for the agent
|
||||
expect(result.agentModelOverride).toBe("ollama/codellama:latest");
|
||||
});
|
||||
|
||||
it("handles malformed Ollama response gracefully", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const fetchSpy = vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "http://127.0.0.1:11434/api/tags") {
|
||||
// Return unexpected structure - models is not an array
|
||||
return new Response(JSON.stringify({ unexpected: "format", models: "not-an-array" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
const note = vi.fn(noopAsync);
|
||||
const confirm = vi.fn(async () => false); // Don't use custom endpoint
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note,
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "ollama",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
// Should treat malformed response as reachable but with no models
|
||||
expect(note).toHaveBeenCalledWith(expect.stringContaining("No models found"), "Ollama");
|
||||
// Should still configure auth profile
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toMatchObject({
|
||||
provider: "ollama",
|
||||
mode: "api_key",
|
||||
});
|
||||
// Should use fallback model
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("ollama/llama3.3");
|
||||
});
|
||||
|
||||
it("writes Qwen credentials when selecting qwen-portal", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
@ -597,6 +1146,10 @@ describe("resolvePreferredProviderForAuthChoice", () => {
|
||||
expect(resolvePreferredProviderForAuthChoice("qwen-portal")).toBe("qwen-portal");
|
||||
});
|
||||
|
||||
it("maps ollama to the provider", () => {
|
||||
expect(resolvePreferredProviderForAuthChoice("ollama")).toBe("ollama");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown choices", () => {
|
||||
expect(resolvePreferredProviderForAuthChoice("unknown" as AuthChoice)).toBeUndefined();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user