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

165 lines
5.7 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { join } from "path";
import { mkdirSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
/**
* getApiServerKeyStatus (vault polish V1) — the richer, ADDITIVE shape behind
* the `get-api-server-key-status` IPC channel, plus the rate-limited
* missing-key diagnostic in getApiServerKey.
*
* Runs the REAL chain (config.yaml → getSecretsProvider →
* CommandSecretsProvider → /bin/sh) against a temp HERMES_HOME with synthetic
* markers, mirroring tests/api-server-key-secrets-provider.test.ts.
*/
const TEST_DIR = join(tmpdir(), `hermes-test-key-status-${Date.now()}`);
const itPosix = process.platform === "win32" ? it.skip : it;
const ORIGINAL_API_SERVER_KEY = process.env.API_SERVER_KEY;
async function freshConfig(
home: string,
): Promise<typeof import("../src/main/config")> {
vi.resetModules();
process.env.HERMES_HOME = home;
return await import("../src/main/config");
}
beforeEach(() => {
mkdirSync(TEST_DIR, { recursive: true });
delete process.env.API_SERVER_KEY;
// Silence the overlay debug line in test output.
vi.spyOn(console, "debug").mockImplementation(() => {});
});
afterEach(() => {
delete process.env.HERMES_HOME;
if (ORIGINAL_API_SERVER_KEY === undefined) delete process.env.API_SERVER_KEY;
else process.env.API_SERVER_KEY = ORIGINAL_API_SERVER_KEY;
vi.restoreAllMocks();
vi.resetModules();
rmSync(TEST_DIR, { recursive: true, force: true });
});
describe("getApiServerKeyStatus", () => {
itPosix(
"reports hasKey=true, providerId='command' for a vault-resolved key",
async () => {
writeFileSync(
join(TEST_DIR, "config.yaml"),
[
"secrets:",
" provider: command",
" command: echo API_SERVER_KEY=synthetic-vault-marker",
"",
].join("\n"),
);
// No .env at all — the key comes only from the provider overlay.
const { getApiServerKeyStatus } = await freshConfig(TEST_DIR);
const status = getApiServerKeyStatus();
expect(status.hasKey).toBe(true);
expect(status.providerId).toBe("command");
expect(typeof status.checkedAt).toBe("number");
},
);
it("reports hasKey=true, providerId='env' for a .env key (no regression)", async () => {
writeFileSync(join(TEST_DIR, "config.yaml"), "agent:\n enabled: true\n");
writeFileSync(
join(TEST_DIR, ".env"),
"API_SERVER_KEY=synthetic-dotenv-marker\n",
);
const { getApiServerKeyStatus } = await freshConfig(TEST_DIR);
const status = getApiServerKeyStatus();
expect(status.hasKey).toBe(true);
expect(status.providerId).toBe("env");
});
itPosix(
"reports hasKey=false with the configured providerId when nothing resolves",
async () => {
writeFileSync(
join(TEST_DIR, "config.yaml"),
["secrets:", " provider: command", ' command: "exit 0"', ""].join(
"\n",
),
);
const { getApiServerKeyStatus } = await freshConfig(TEST_DIR);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const status = getApiServerKeyStatus();
expect(status.hasKey).toBe(false);
expect(status.providerId).toBe("command");
warnSpy.mockRestore();
},
);
it("keeps hasKey as the required primary field (additive contract)", async () => {
writeFileSync(join(TEST_DIR, "config.yaml"), "agent:\n enabled: true\n");
const { getApiServerKeyStatus } = await freshConfig(TEST_DIR);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const status = getApiServerKeyStatus();
// Old renderer code reads exactly this field — it must always be present.
expect(Object.prototype.hasOwnProperty.call(status, "hasKey")).toBe(true);
expect(status.hasKey).toBe(false);
warnSpy.mockRestore();
});
});
describe("getApiServerKey missing-key diagnostic", () => {
itPosix(
"warns exactly once per (provider, profile) pair, naming the provider",
async () => {
writeFileSync(
join(TEST_DIR, "config.yaml"),
["secrets:", " provider: command", ' command: "exit 0"', ""].join(
"\n",
),
);
const { getApiServerKey, invalidateSecretsCache } =
await freshConfig(TEST_DIR);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(getApiServerKey()).toBe("");
// Force a real re-resolution (the 5s cache would otherwise short-circuit
// before the diagnostic) — the rate-limit Set must still suppress it.
invalidateSecretsCache();
expect(getApiServerKey()).toBe("");
const diagnostics = warnSpy.mock.calls
.flat()
.filter(
(m) =>
typeof m === "string" && m.includes("API_SERVER_KEY not resolved"),
);
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0]).toContain("provider=command");
expect(diagnostics[0]).toContain("env=default");
warnSpy.mockRestore();
},
);
it("does not warn when the key resolves", async () => {
writeFileSync(join(TEST_DIR, "config.yaml"), "agent:\n enabled: true\n");
writeFileSync(
join(TEST_DIR, ".env"),
"API_SERVER_KEY=synthetic-present-marker\n",
);
const { getApiServerKey } = await freshConfig(TEST_DIR);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(getApiServerKey()).toBe("synthetic-present-marker");
const diagnostics = warnSpy.mock.calls
.flat()
.filter(
(m) =>
typeof m === "string" && m.includes("API_SERVER_KEY not resolved"),
);
expect(diagnostics).toHaveLength(0);
warnSpy.mockRestore();
});
});