Files
fathah--hermes-desktop/tests/api-server-key-resolution.test.ts
2026-07-13 13:36:53 +08:00

160 lines
5.3 KiB
TypeScript

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<typeof resolveApiServerKey>[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");
});
});