import { describe, expect, it } from "vitest"; import { resolveApiServerKey } from "../src/main/config"; /** * Issue #333 fix — `resolveApiServerKey` decides which of six candidate * locations wins when the desktop builds the `Authorization: Bearer …` * header for its chat / session-continuation requests. * * The precedence has to satisfy two requirements simultaneously: * * 1. The documented `.env` workaround keeps working — a user who * manually pastes `API_SERVER_KEY=…` into `.env` (to unblock the * second-message failure on a fresh `hermes setup` install) must * see that value win over whatever was generated by setup into * `api_server.token`. * * 2. The canonical hermes-agent gateway-secret location * (`api_server.token` in `config.yaml`) must be honoured as a * fallback — without this, a user who has never edited `.env` but * has run `hermes setup` (which writes `api_server.token`) sees * "Session continuation requires API key authentication" on the * second chat message, even though the secret is right there in * `config.yaml`. * * The candidate order is: * * 1. Profile `config.yaml` top-level `API_SERVER_KEY` (legacy override) * 2. Default `config.yaml` top-level `API_SERVER_KEY` (legacy override) * 3. Profile `.env` `API_SERVER_KEY` * 4. Default `.env` `API_SERVER_KEY` * 5. Profile `config.yaml` `api_server.token` (canonical) * 6. Default `config.yaml` `api_server.token` */ function empty(): Parameters[0] { return { configTopLevelProfile: null, configTopLevelDefault: null, envProfile: null, envDefault: null, apiServerTokenProfile: null, apiServerTokenDefault: null, }; } describe("resolveApiServerKey", () => { it("returns empty when nothing is configured", () => { expect(resolveApiServerKey(empty())).toBe(""); }); it("honours api_server.token when no .env key is present (the #333 fix)", () => { const result = resolveApiServerKey({ ...empty(), apiServerTokenProfile: "token-from-config-yaml", }); expect(result).toBe("token-from-config-yaml"); }); it(".env API_SERVER_KEY wins over api_server.token (preserves the manual workaround)", () => { const result = resolveApiServerKey({ ...empty(), envProfile: "env-key-workaround", apiServerTokenProfile: "token-in-config", }); expect(result).toBe("env-key-workaround"); }); it("legacy top-level API_SERVER_KEY in config.yaml wins over both .env and api_server.token", () => { const result = resolveApiServerKey({ ...empty(), configTopLevelProfile: "legacy-top-level", envProfile: "env-value", apiServerTokenProfile: "token-value", }); expect(result).toBe("legacy-top-level"); }); it("profile-scoped sources beat default-scoped sources at the same layer", () => { expect( resolveApiServerKey({ ...empty(), configTopLevelProfile: "profile-top-level", configTopLevelDefault: "default-top-level", }), ).toBe("profile-top-level"); expect( resolveApiServerKey({ ...empty(), envProfile: "profile-env", envDefault: "default-env", }), ).toBe("profile-env"); expect( resolveApiServerKey({ ...empty(), apiServerTokenProfile: "profile-token", apiServerTokenDefault: "default-token", }), ).toBe("profile-token"); }); it("falls through to default-scoped api_server.token when profile sources are all empty", () => { const result = resolveApiServerKey({ ...empty(), apiServerTokenDefault: "default-token", }); expect(result).toBe("default-token"); }); it("trims whitespace from the winning candidate", () => { expect( resolveApiServerKey({ ...empty(), apiServerTokenProfile: " padded-token \n", }), ).toBe("padded-token"); }); it("treats whitespace-only candidates as empty and falls through to the next", () => { const result = resolveApiServerKey({ ...empty(), envProfile: " ", apiServerTokenProfile: "real-token", }); expect(result).toBe("real-token"); }); it("handles the realistic 'fresh `hermes setup`' shape end-to-end", () => { // Mirrors the observed symptom: hermes setup wrote api_server.token // into config.yaml; .env is untouched; no legacy top-level override. const result = resolveApiServerKey({ configTopLevelProfile: null, configTopLevelDefault: null, envProfile: null, envDefault: null, apiServerTokenProfile: "setup-generated-token", apiServerTokenDefault: null, }); expect(result).toBe("setup-generated-token"); }); it("handles the 'user applied the .env workaround on top of fresh setup' shape", () => { // motivix's reported fix: keep api_server.token in config.yaml AND // paste the same value into .env as API_SERVER_KEY. With both // present, .env should win — same value is fine, but order matters // for the case where they drift. const result = resolveApiServerKey({ configTopLevelProfile: null, configTopLevelDefault: null, envProfile: "manually-pasted-value", envDefault: null, apiServerTokenProfile: "setup-generated-token", apiServerTokenDefault: null, }); expect(result).toBe("manually-pasted-value"); }); });