/** * L5 — pure first-run profile logic: validation mirrors of the backend invariants * (`validateProfile`/`isProfileValid`), `isRelativeSafe`, `isValidEnvVar`, * `defaultInjection`, `emptyCustomProfile`, and `parseArgs`. */ import { describe, it, expect } from "vitest"; import type { AgentProfile } from "@/domain"; import { defaultHttpChatConfig, defaultInjection, defaultOpenCodeConfig, emptyCustomProfile, isProfileValid, isRelativeSafe, isValidEnvVar, isValidHttpUrl, parseArgs, validateHttpChatConfig, validateOpenCodeConfig, validateProfile, } from "./profile"; function base(overrides: Partial = {}): AgentProfile { return { id: "p1", name: "Claude", command: "claude", args: [], contextInjection: { strategy: "conventionFile", target: "CLAUDE.md" }, detect: null, cwdTemplate: "{projectRoot}", ...overrides, }; } describe("isRelativeSafe", () => { it("accepts a plain relative file name", () => { expect(isRelativeSafe("CLAUDE.md")).toBe(true); expect(isRelativeSafe("docs/AGENTS.md")).toBe(true); }); it("rejects empty, absolute, traversal and drive paths", () => { expect(isRelativeSafe("")).toBe(false); expect(isRelativeSafe("/etc/passwd")).toBe(false); expect(isRelativeSafe("\\windows")).toBe(false); expect(isRelativeSafe("../secret")).toBe(false); expect(isRelativeSafe("a/../b")).toBe(false); expect(isRelativeSafe("C:/tmp")).toBe(false); }); }); describe("isValidEnvVar", () => { it("accepts valid identifiers", () => { expect(isValidEnvVar("AGENT_CONTEXT")).toBe(true); expect(isValidEnvVar("_x1")).toBe(true); }); it("rejects invalid identifiers", () => { expect(isValidEnvVar("")).toBe(false); expect(isValidEnvVar("1ABC")).toBe(false); expect(isValidEnvVar("HAS-DASH")).toBe(false); expect(isValidEnvVar("has space")).toBe(false); }); }); describe("validateProfile / isProfileValid", () => { it("a well-formed profile is valid", () => { expect(validateProfile(base())).toEqual({}); expect(isProfileValid(base())).toBe(true); }); it("blank name and command are reported", () => { const errors = validateProfile(base({ name: " ", command: "" })); expect(errors.name).toBeDefined(); expect(errors.command).toBeDefined(); expect(isProfileValid(base({ name: "" }))).toBe(false); }); it("convention file target must be relative-safe", () => { const errors = validateProfile( base({ contextInjection: { strategy: "conventionFile", target: "../x" } }), ); expect(errors.target).toBeDefined(); }); it("flag must be non-empty", () => { const errors = validateProfile( base({ contextInjection: { strategy: "flag", flag: " " } }), ); expect(errors.flag).toBeDefined(); }); it("env var must be a valid identifier", () => { const errors = validateProfile( base({ contextInjection: { strategy: "env", var: "1bad" } }), ); expect(errors.var).toBeDefined(); }); it("stdin needs no extra field", () => { expect( isProfileValid(base({ contextInjection: { strategy: "stdin" } })), ).toBe(true); }); }); 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 { 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("validateOpenCodeConfig", () => { it("a well-formed llama.cpp config is valid", () => { expect(validateOpenCodeConfig(defaultOpenCodeConfig())).toEqual({}); }); it("reports a bad base URL scheme and an empty model", () => { const errors = validateOpenCodeConfig({ baseURL: "ftp://x", model: " " }); expect(errors.baseURL).toBeDefined(); expect(errors.model).toBeDefined(); }); it("an optional API key is free-form (no env-var-name constraint)", () => { // Unlike the openAiCompatible adapter, OpenCode carries the key itself. expect( validateOpenCodeConfig({ baseURL: "http://localhost:8080/v1", apiKey: "sk-secret-123", model: "qwen3-coder-30b", }), ).toEqual({}); }); }); describe("validateProfile for openCode profiles", () => { function opencode(overrides: Partial = {}): AgentProfile { return base({ name: "OpenCode + llama.cpp", command: "opencode", structuredAdapter: "openCode", opencode: defaultOpenCodeConfig(), ...overrides, }); } it("the reference llama.cpp profile is valid", () => { expect(validateProfile(opencode())).toEqual({}); expect(isProfileValid(opencode())).toBe(true); }); it("surfaces opencode field errors through the profile validator", () => { const errors = validateProfile( opencode({ opencode: { baseURL: "nope", model: "" } }), ); expect(errors.baseURL).toBeDefined(); expect(errors.model).toBeDefined(); expect( isProfileValid(opencode({ opencode: { baseURL: "nope", model: "m" } })), ).toBe(false); }); it("a missing opencode config on an openCode profile is invalid", () => { const errors = validateProfile(opencode({ opencode: undefined })); expect(errors.baseURL).toBeDefined(); expect(errors.model).toBeDefined(); }); }); describe("defaultInjection", () => { it("produces a sensible default per strategy", () => { expect(defaultInjection("conventionFile")).toEqual({ strategy: "conventionFile", target: "CONTEXT.md", }); expect(defaultInjection("flag")).toEqual({ strategy: "flag", flag: "--context-file {path}", }); expect(defaultInjection("env")).toEqual({ strategy: "env", var: "AGENT_CONTEXT_FILE", }); expect(defaultInjection("stdin")).toEqual({ strategy: "stdin" }); }); }); describe("emptyCustomProfile", () => { it("is a blank, invalid draft with a fresh id", () => { const a = emptyCustomProfile(); const b = emptyCustomProfile(); expect(a.name).toBe(""); expect(a.command).toBe(""); expect(isProfileValid(a)).toBe(false); expect(a.id).not.toEqual(b.id); }); }); describe("parseArgs", () => { it("splits on whitespace, trims and drops empties", () => { expect(parseArgs(" --foo bar\t baz ")).toEqual(["--foo", "bar", "baz"]); expect(parseArgs("")).toEqual([]); expect(parseArgs(" ")).toEqual([]); }); });