+ );
+}
diff --git a/frontend/src/features/first-run/profile.test.ts b/frontend/src/features/first-run/profile.test.ts
index aaee781..cc1c91c 100644
--- a/frontend/src/features/first-run/profile.test.ts
+++ b/frontend/src/features/first-run/profile.test.ts
@@ -7,12 +7,15 @@ import { describe, it, expect } from "vitest";
import type { AgentProfile } from "@/domain";
import {
+ defaultHttpChatConfig,
defaultInjection,
emptyCustomProfile,
isProfileValid,
isRelativeSafe,
isValidEnvVar,
+ isValidHttpUrl,
parseArgs,
+ validateHttpChatConfig,
validateProfile,
} from "./profile";
@@ -93,6 +96,99 @@ describe("validateProfile / isProfileValid", () => {
});
});
+describe("isValidHttpUrl", () => {
+ it("accepts http/https endpoints", () => {
+ expect(isValidHttpUrl("http://localhost:11434/v1")).toBe(true);
+ expect(isValidHttpUrl("https://lan.host:8080/v1")).toBe(true);
+ });
+ it("rejects empty, non-http and other schemes", () => {
+ expect(isValidHttpUrl("")).toBe(false);
+ expect(isValidHttpUrl(" ")).toBe(false);
+ expect(isValidHttpUrl("ftp://host")).toBe(false);
+ expect(isValidHttpUrl("localhost:11434")).toBe(false);
+ // Leading whitespace fails the raw prefix check, mirroring the backend.
+ expect(isValidHttpUrl(" http://host")).toBe(false);
+ });
+});
+
+describe("validateHttpChatConfig", () => {
+ it("a well-formed local model config is valid", () => {
+ expect(validateHttpChatConfig(defaultHttpChatConfig())).toEqual({});
+ });
+ it("reports a bad endpoint scheme and an empty model", () => {
+ const errors = validateHttpChatConfig({ endpoint: "ftp://x", model: " " });
+ expect(errors.endpoint).toBeDefined();
+ expect(errors.model).toBeDefined();
+ });
+ it("apiKeyEnv must be an env var NAME, never a raw key", () => {
+ // A plausible raw key contains characters illegal in an env var identifier.
+ const withKey = validateHttpChatConfig({
+ endpoint: "http://host",
+ model: "m",
+ apiKeyEnv: "sk-abc123",
+ });
+ expect(withKey.apiKeyEnv).toBeDefined();
+ // A valid identifier passes; blank/undefined is allowed (optional).
+ expect(
+ validateHttpChatConfig({
+ endpoint: "http://host",
+ model: "m",
+ apiKeyEnv: "OPENAI_API_KEY",
+ }).apiKeyEnv,
+ ).toBeUndefined();
+ expect(
+ validateHttpChatConfig({ endpoint: "http://host", model: "m" }).apiKeyEnv,
+ ).toBeUndefined();
+ });
+ it("rejects zero / non-integer timeouts and tool-iteration guard", () => {
+ const errors = validateHttpChatConfig({
+ endpoint: "http://host",
+ model: "m",
+ requestTimeoutMs: 0,
+ connectTimeoutMs: -1,
+ maxToolIterations: 1.5,
+ });
+ expect(errors.requestTimeoutMs).toBeDefined();
+ expect(errors.connectTimeoutMs).toBeDefined();
+ expect(errors.maxToolIterations).toBeDefined();
+ });
+});
+
+describe("validateProfile for openAiCompatible profiles", () => {
+ function ollama(overrides: Partial