160 lines
5.3 KiB
TypeScript
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");
|
|
});
|
|
});
|