feat(ui): profils locaux/LAN OpenAI-compatible dans le first-run et les terminaux (#14)
Câble la surface frontend des profils IA locaux/LAN OpenAI-compatible,
en parité avec l'adapter backend additif (aab4bca).
- domain: types de profil OpenAI-compatible
- first-run: édition/validation du profil dans le FirstRunWizard
- adapters/mock: mock de profil pour les tests
- terminals: rendu des round-trips et erreurs endpoint (role=alert)
Validé QA (frontend GO): typecheck exit 0, vitest 59 fichiers / 566 tests,
0 echec ; couverture timeouts round-trip + erreur endpoint role=alert
prouvees ; pas de regression sur les tests de bail headless.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -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<AgentProfile> = {}): AgentProfile {
|
||||
return base({
|
||||
name: "Ollama",
|
||||
command: "openai-compatible",
|
||||
structuredAdapter: "openAiCompatible",
|
||||
chatHttp: defaultHttpChatConfig(),
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
it("the reference local-model profile is valid", () => {
|
||||
expect(validateProfile(ollama())).toEqual({});
|
||||
expect(isProfileValid(ollama())).toBe(true);
|
||||
});
|
||||
it("surfaces chatHttp field errors through the profile validator", () => {
|
||||
const errors = validateProfile(
|
||||
ollama({ chatHttp: { endpoint: "nope", model: "" } }),
|
||||
);
|
||||
expect(errors.endpoint).toBeDefined();
|
||||
expect(errors.model).toBeDefined();
|
||||
expect(isProfileValid(ollama({ chatHttp: { endpoint: "nope", model: "m" } }))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it("a missing chatHttp on an openAiCompatible profile is invalid", () => {
|
||||
const errors = validateProfile(ollama({ chatHttp: undefined }));
|
||||
expect(errors.endpoint).toBeDefined();
|
||||
expect(errors.model).toBeDefined();
|
||||
});
|
||||
it("does not touch validation of Claude/Codex (non-structured-http) profiles", () => {
|
||||
// A plain profile with no structuredAdapter never triggers chatHttp checks.
|
||||
expect(validateProfile(base())).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultInjection", () => {
|
||||
it("produces a sensible default per strategy", () => {
|
||||
expect(defaultInjection("conventionFile")).toEqual({
|
||||
|
||||
Reference in New Issue
Block a user