refactor(tests): consolidate repeated setup helpers

This commit is contained in:
Peter Steinberger
2026-03-03 01:05:30 +00:00
parent a48a3dbdda
commit 57e1534df8
8 changed files with 494 additions and 524 deletions

View File

@@ -44,12 +44,40 @@ function createPromptSpies(params?: { confirmResult?: boolean; textResult?: stri
return { confirm, note, text };
}
function createPromptAndCredentialSpies(params?: { confirmResult?: boolean; textResult?: string }) {
return {
...createPromptSpies(params),
setCredential: vi.fn(async () => undefined),
};
}
async function ensureMinimaxApiKey(params: {
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
confirm: WizardPrompter["confirm"];
note?: WizardPrompter["note"];
select?: WizardPrompter["select"];
text: WizardPrompter["text"];
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
secretInputMode?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["secretInputMode"];
}) {
return await ensureMinimaxApiKeyInternal({
config: params.config,
prompter: createPrompter({
confirm: params.confirm,
note: params.note,
select: params.select,
text: params.text,
}),
secretInputMode: params.secretInputMode,
setCredential: params.setCredential,
});
}
async function ensureMinimaxApiKeyInternal(params: {
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
prompter: WizardPrompter;
secretInputMode?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["secretInputMode"];
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
}) {
return await ensureApiKeyFromEnvOrPrompt({
config: params.config ?? {},
@@ -58,7 +86,7 @@ async function ensureMinimaxApiKey(params: {
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm: params.confirm, text: params.text }),
prompter: params.prompter,
secretInputMode: params.secretInputMode,
setCredential: params.setCredential,
});
@@ -71,13 +99,8 @@ async function ensureMinimaxApiKeyWithEnvRefPrompter(params: {
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
text: WizardPrompter["text"];
}) {
return await ensureApiKeyFromEnvOrPrompt({
config: params.config ?? {},
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
return await ensureMinimaxApiKeyInternal({
config: params.config,
prompter: createPrompter({ select: params.select, text: params.text, note: params.note }),
secretInputMode: "ref",
setCredential: params.setCredential,
@@ -102,6 +125,55 @@ async function runEnsureMinimaxApiKeyFlow(params: { confirmResult: boolean; text
return { result, setCredential, confirm, text };
}
async function runMaybeApplyHuggingFaceToken(tokenProvider: string) {
const setCredential = vi.fn(async () => undefined);
const result = await maybeApplyApiKeyFromOption({
token: " opt-key ",
tokenProvider,
expectedProviders: ["huggingface"],
normalize: (value) => value.trim(),
setCredential,
});
return { result, setCredential };
}
function expectMinimaxEnvRefCredentialStored(setCredential: ReturnType<typeof vi.fn>) {
expect(setCredential).toHaveBeenCalledWith(
{ source: "env", provider: "default", id: "MINIMAX_API_KEY" },
"ref",
);
}
async function ensureWithOptionEnvOrPrompt(params: {
token: string;
tokenProvider: string;
expectedProviders: string[];
provider: string;
envLabel: string;
confirm: WizardPrompter["confirm"];
note: WizardPrompter["note"];
noteMessage: string;
noteTitle: string;
setCredential: Parameters<typeof ensureApiKeyFromOptionEnvOrPrompt>[0]["setCredential"];
text: WizardPrompter["text"];
}) {
return await ensureApiKeyFromOptionEnvOrPrompt({
token: params.token,
tokenProvider: params.tokenProvider,
config: {},
expectedProviders: params.expectedProviders,
provider: params.provider,
envLabel: params.envLabel,
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm: params.confirm, note: params.note, text: params.text }),
setCredential: params.setCredential,
noteMessage: params.noteMessage,
noteTitle: params.noteTitle,
});
}
afterEach(() => {
restoreMinimaxEnv();
vi.restoreAllMocks();
@@ -116,30 +188,14 @@ describe("normalizeTokenProviderInput", () => {
describe("maybeApplyApiKeyFromOption", () => {
it("stores normalized token when provider matches", async () => {
const setCredential = vi.fn(async () => undefined);
const result = await maybeApplyApiKeyFromOption({
token: " opt-key ",
tokenProvider: "huggingface",
expectedProviders: ["huggingface"],
normalize: (value) => value.trim(),
setCredential,
});
const { result, setCredential } = await runMaybeApplyHuggingFaceToken("huggingface");
expect(result).toBe("opt-key");
expect(setCredential).toHaveBeenCalledWith("opt-key", undefined);
});
it("matches provider with whitespace/case normalization", async () => {
const setCredential = vi.fn(async () => undefined);
const result = await maybeApplyApiKeyFromOption({
token: " opt-key ",
tokenProvider: " HuGgInGfAcE ",
expectedProviders: ["huggingface"],
normalize: (value) => value.trim(),
setCredential,
});
const { result, setCredential } = await runMaybeApplyHuggingFaceToken(" HuGgInGfAcE ");
expect(result).toBe("opt-key");
expect(setCredential).toHaveBeenCalledWith("opt-key", undefined);
@@ -192,11 +248,10 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
process.env.MINIMAX_API_KEY = "env-key";
delete process.env.MINIMAX_OAUTH_TOKEN;
const { confirm, text } = createPromptSpies({
const { confirm, text, setCredential } = createPromptAndCredentialSpies({
confirmResult: true,
textResult: "prompt-key",
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureMinimaxApiKey({
confirm,
@@ -206,10 +261,7 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
});
expect(result).toBe("env-key");
expect(setCredential).toHaveBeenCalledWith(
{ source: "env", provider: "default", id: "MINIMAX_API_KEY" },
"ref",
);
expectMinimaxEnvRefCredentialStored(setCredential);
expect(text).not.toHaveBeenCalled();
});
@@ -217,11 +269,10 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
delete process.env.MINIMAX_API_KEY;
delete process.env.MINIMAX_OAUTH_TOKEN;
const { confirm, text } = createPromptSpies({
const { confirm, text, setCredential } = createPromptAndCredentialSpies({
confirmResult: true,
textResult: "prompt-key",
});
const setCredential = vi.fn(async () => undefined);
await expect(
ensureMinimaxApiKey({
@@ -268,10 +319,7 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
});
expect(result).toBe("env-key");
expect(setCredential).toHaveBeenCalledWith(
{ source: "env", provider: "default", id: "MINIMAX_API_KEY" },
"ref",
);
expectMinimaxEnvRefCredentialStored(setCredential);
expect(note).toHaveBeenCalledWith(
expect.stringContaining("Could not validate provider reference"),
"Reference check failed",
@@ -304,26 +352,23 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
describe("ensureApiKeyFromOptionEnvOrPrompt", () => {
it("uses opts token and skips note/env/prompt", async () => {
const { confirm, note, text } = createPromptSpies({
const { confirm, note, text, setCredential } = createPromptAndCredentialSpies({
confirmResult: true,
textResult: "prompt-key",
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureApiKeyFromOptionEnvOrPrompt({
const result = await ensureWithOptionEnvOrPrompt({
token: " opts-key ",
tokenProvider: " HUGGINGFACE ",
config: {},
expectedProviders: ["huggingface"],
provider: "huggingface",
envLabel: "HF_TOKEN",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, note, text }),
setCredential,
confirm,
note,
noteMessage: "Hugging Face note",
noteTitle: "Hugging Face",
setCredential,
text,
});
expect(result).toBe("opts-key");
@@ -337,26 +382,23 @@ describe("ensureApiKeyFromOptionEnvOrPrompt", () => {
delete process.env.MINIMAX_OAUTH_TOKEN;
process.env.MINIMAX_API_KEY = "env-key";
const { confirm, note, text } = createPromptSpies({
const { confirm, note, text, setCredential } = createPromptAndCredentialSpies({
confirmResult: true,
textResult: "prompt-key",
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureApiKeyFromOptionEnvOrPrompt({
const result = await ensureWithOptionEnvOrPrompt({
token: "opts-key",
tokenProvider: "openai",
config: {},
expectedProviders: ["minimax"],
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, note, text }),
setCredential,
confirm,
note,
noteMessage: "MiniMax note",
noteTitle: "MiniMax",
setCredential,
text,
});
expect(result).toBe("env-key");

View File

@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { withEnvAsync } from "../test-utils/env.js";
const loadConfig = vi.fn(() => ({
@@ -134,15 +135,33 @@ function createRuntimeCapture() {
return { runtime, runtimeLogs, runtimeErrors };
}
function asRuntimeEnv(runtime: ReturnType<typeof createRuntimeCapture>["runtime"]): RuntimeEnv {
return runtime as unknown as RuntimeEnv;
}
function makeRemoteGatewayConfig(url: string, token = "rtok", localToken = "ltok") {
return {
gateway: {
mode: "remote",
remote: { url, token },
auth: { token: localToken },
},
};
}
async function runGatewayStatus(
runtime: ReturnType<typeof createRuntimeCapture>["runtime"],
opts: { timeout: string; json?: boolean; ssh?: string; sshAuto?: boolean; sshIdentity?: string },
) {
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(opts, asRuntimeEnv(runtime));
}
describe("gateway-status command", () => {
it("prints human output by default", async () => {
const { runtime, runtimeLogs, runtimeErrors } = createRuntimeCapture();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000" },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, { timeout: "1000" });
expect(runtimeErrors).toHaveLength(0);
expect(runtimeLogs.join("\n")).toContain("Gateway Status");
@@ -153,11 +172,7 @@ describe("gateway-status command", () => {
it("prints a structured JSON envelope when --json is set", async () => {
const { runtime, runtimeLogs, runtimeErrors } = createRuntimeCapture();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000", json: true },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, { timeout: "1000", json: true });
expect(runtimeErrors).toHaveLength(0);
const parsed = JSON.parse(runtimeLogs.join("\n")) as Record<string, unknown>;
@@ -176,11 +191,7 @@ describe("gateway-status command", () => {
sshStop.mockClear();
probeGateway.mockClear();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000", json: true, ssh: "me@studio" },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, { timeout: "1000", json: true, ssh: "me@studio" });
expect(startSshPortForward).toHaveBeenCalledTimes(1);
expect(probeGateway).toHaveBeenCalled();
@@ -198,24 +209,14 @@ describe("gateway-status command", () => {
it("skips invalid ssh-auto discovery targets", async () => {
const { runtime } = createRuntimeCapture();
await withEnvAsync({ USER: "steipete" }, async () => {
loadConfig.mockReturnValueOnce({
gateway: {
mode: "remote",
remote: { url: "", token: "" },
auth: { token: "ltok" },
},
});
loadConfig.mockReturnValueOnce(makeRemoteGatewayConfig("", "", "ltok"));
discoverGatewayBeacons.mockResolvedValueOnce([
{ tailnetDns: "-V" },
{ tailnetDns: "goodhost" },
]);
startSshPortForward.mockClear();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000", json: true, sshAuto: true },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, { timeout: "1000", json: true, sshAuto: true });
expect(startSshPortForward).toHaveBeenCalledTimes(1);
const call = startSshPortForward.mock.calls[0]?.[0] as { target: string };
@@ -226,13 +227,9 @@ describe("gateway-status command", () => {
it("infers SSH target from gateway.remote.url and ssh config", async () => {
const { runtime } = createRuntimeCapture();
await withEnvAsync({ USER: "steipete" }, async () => {
loadConfig.mockReturnValueOnce({
gateway: {
mode: "remote",
remote: { url: "ws://peters-mac-studio-1.sheep-coho.ts.net:18789", token: "rtok" },
auth: { token: "ltok" },
},
});
loadConfig.mockReturnValueOnce(
makeRemoteGatewayConfig("ws://peters-mac-studio-1.sheep-coho.ts.net:18789"),
);
resolveSshConfig.mockResolvedValueOnce({
user: "steipete",
host: "peters-mac-studio-1.sheep-coho.ts.net",
@@ -241,11 +238,7 @@ describe("gateway-status command", () => {
});
startSshPortForward.mockClear();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000", json: true },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, { timeout: "1000", json: true });
expect(startSshPortForward).toHaveBeenCalledTimes(1);
const call = startSshPortForward.mock.calls[0]?.[0] as {
@@ -260,21 +253,11 @@ describe("gateway-status command", () => {
it("falls back to host-only when USER is missing and ssh config is unavailable", async () => {
const { runtime } = createRuntimeCapture();
await withEnvAsync({ USER: "" }, async () => {
loadConfig.mockReturnValueOnce({
gateway: {
mode: "remote",
remote: { url: "wss://studio.example:18789", token: "rtok" },
auth: { token: "ltok" },
},
});
loadConfig.mockReturnValueOnce(makeRemoteGatewayConfig("wss://studio.example:18789"));
resolveSshConfig.mockResolvedValueOnce(null);
startSshPortForward.mockClear();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000", json: true },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, { timeout: "1000", json: true });
const call = startSshPortForward.mock.calls[0]?.[0] as {
target: string;
@@ -286,13 +269,7 @@ describe("gateway-status command", () => {
it("keeps explicit SSH identity even when ssh config provides one", async () => {
const { runtime } = createRuntimeCapture();
loadConfig.mockReturnValueOnce({
gateway: {
mode: "remote",
remote: { url: "wss://studio.example:18789", token: "rtok" },
auth: { token: "ltok" },
},
});
loadConfig.mockReturnValueOnce(makeRemoteGatewayConfig("wss://studio.example:18789"));
resolveSshConfig.mockResolvedValueOnce({
user: "me",
host: "studio.example",
@@ -301,11 +278,11 @@ describe("gateway-status command", () => {
});
startSshPortForward.mockClear();
const { gatewayStatusCommand } = await import("./gateway-status.js");
await gatewayStatusCommand(
{ timeout: "1000", json: true, sshIdentity: "/tmp/explicit_id" },
runtime as unknown as import("../runtime.js").RuntimeEnv,
);
await runGatewayStatus(runtime, {
timeout: "1000",
json: true,
sshIdentity: "/tmp/explicit_id",
});
const call = startSshPortForward.mock.calls[0]?.[0] as {
identity?: string;