Add CLI and infra test coverage
This commit is contained in:
91
src/cli/program.test.ts
Normal file
91
src/cli/program.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendCommand = vi.fn();
|
||||
const statusCommand = vi.fn();
|
||||
const upCommand = vi.fn().mockResolvedValue({ server: undefined });
|
||||
const webhookCommand = vi.fn().mockResolvedValue(undefined);
|
||||
const ensureTwilioEnv = vi.fn();
|
||||
const loginWeb = vi.fn();
|
||||
const monitorWebProvider = vi.fn();
|
||||
const pickProvider = vi.fn();
|
||||
const monitorTwilio = vi.fn();
|
||||
const logTwilioFrom = vi.fn();
|
||||
const logWebSelfId = vi.fn();
|
||||
const waitForever = vi.fn();
|
||||
const spawnRelayTmux = vi.fn().mockResolvedValue("warelay-relay");
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(() => {
|
||||
throw new Error("exit");
|
||||
}),
|
||||
};
|
||||
|
||||
vi.mock("../commands/send.js", () => ({ sendCommand }));
|
||||
vi.mock("../commands/status.js", () => ({ statusCommand }));
|
||||
vi.mock("../commands/up.js", () => ({ upCommand }));
|
||||
vi.mock("../commands/webhook.js", () => ({ webhookCommand }));
|
||||
vi.mock("../env.js", () => ({ ensureTwilioEnv }));
|
||||
vi.mock("../runtime.js", () => ({ defaultRuntime: runtime }));
|
||||
vi.mock("../provider-web.js", () => ({
|
||||
loginWeb,
|
||||
monitorWebProvider,
|
||||
pickProvider,
|
||||
}));
|
||||
vi.mock("./deps.js", () => ({
|
||||
createDefaultDeps: () => ({ waitForever }),
|
||||
logTwilioFrom,
|
||||
logWebSelfId,
|
||||
monitorTwilio,
|
||||
}));
|
||||
vi.mock("./relay_tmux.js", () => ({ spawnRelayTmux }));
|
||||
|
||||
const { buildProgram } = await import("./program.js");
|
||||
|
||||
describe("cli program", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("runs send with required options", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(["send", "--to", "+1", "--message", "hi"], {
|
||||
from: "user",
|
||||
});
|
||||
expect(sendCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects invalid relay provider", async () => {
|
||||
const program = buildProgram();
|
||||
await expect(
|
||||
program.parseAsync(["relay", "--provider", "bogus"], { from: "user" }),
|
||||
).rejects.toThrow("exit");
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"--provider must be auto, web, or twilio",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to twilio when web relay fails", async () => {
|
||||
pickProvider.mockResolvedValue("web");
|
||||
monitorWebProvider.mockRejectedValue(new Error("no web"));
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(
|
||||
["relay", "--provider", "auto", "--interval", "2", "--lookback", "1"],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(logWebSelfId).toHaveBeenCalled();
|
||||
expect(ensureTwilioEnv).toHaveBeenCalled();
|
||||
expect(monitorTwilio).toHaveBeenCalledWith(2, 1);
|
||||
});
|
||||
|
||||
it("runs relay tmux attach command", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(["relay:tmux:attach"], { from: "user" });
|
||||
expect(spawnRelayTmux).toHaveBeenCalledWith(
|
||||
"pnpm warelay relay --verbose",
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
49
src/cli/prompt.test.ts
Normal file
49
src/cli/prompt.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { isYes, setVerbose, setYes } from "../globals.js";
|
||||
|
||||
vi.mock("node:readline/promises", () => {
|
||||
const question = vi.fn<[], Promise<string>>();
|
||||
const close = vi.fn();
|
||||
const createInterface = vi.fn(() => ({ question, close }));
|
||||
return { default: { createInterface } };
|
||||
});
|
||||
|
||||
type ReadlineMock = {
|
||||
default: {
|
||||
createInterface: () => {
|
||||
question: ReturnType<typeof vi.fn<[], Promise<string>>>;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const { promptYesNo } = await import("./prompt.js");
|
||||
const readline = (await import("node:readline/promises")) as ReadlineMock;
|
||||
|
||||
describe("promptYesNo", () => {
|
||||
it("returns true when global --yes is set", async () => {
|
||||
setYes(true);
|
||||
setVerbose(false);
|
||||
const result = await promptYesNo("Continue?");
|
||||
expect(result).toBe(true);
|
||||
expect(isYes()).toBe(true);
|
||||
});
|
||||
|
||||
it("asks the question and respects default", async () => {
|
||||
setYes(false);
|
||||
setVerbose(false);
|
||||
const { question: questionMock } = readline.default.createInterface();
|
||||
questionMock.mockResolvedValueOnce("");
|
||||
const resultDefaultYes = await promptYesNo("Continue?", true);
|
||||
expect(resultDefaultYes).toBe(true);
|
||||
|
||||
questionMock.mockResolvedValueOnce("n");
|
||||
const resultNo = await promptYesNo("Continue?", true);
|
||||
expect(resultNo).toBe(false);
|
||||
|
||||
questionMock.mockResolvedValueOnce("y");
|
||||
const resultYes = await promptYesNo("Continue?", false);
|
||||
expect(resultYes).toBe(true);
|
||||
});
|
||||
});
|
||||
47
src/cli/relay_tmux.test.ts
Normal file
47
src/cli/relay_tmux.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", () => {
|
||||
const spawn = vi.fn((_cmd: string, _args: string[]) => {
|
||||
const proc = new EventEmitter() as EventEmitter & {
|
||||
kill: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
queueMicrotask(() => {
|
||||
proc.emit("exit", 0);
|
||||
});
|
||||
proc.kill = vi.fn();
|
||||
return proc;
|
||||
});
|
||||
return { spawn };
|
||||
});
|
||||
|
||||
const { spawnRelayTmux } = await import("./relay_tmux.js");
|
||||
const { spawn } = await import("node:child_process");
|
||||
|
||||
describe("spawnRelayTmux", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("kills old session, starts new one, and attaches", async () => {
|
||||
const session = await spawnRelayTmux("echo hi", true, true);
|
||||
expect(session).toBe("warelay-relay");
|
||||
const spawnMock = spawn as unknown as vi.Mock;
|
||||
expect(spawnMock.mock.calls.length).toBe(3);
|
||||
const calls = spawnMock.mock.calls as Array<[string, string[], unknown]>;
|
||||
expect(calls[0][0]).toBe("tmux"); // kill-session
|
||||
expect(calls[1][2]?.cmd ?? "").not.toBeUndefined(); // new session
|
||||
expect(calls[2][1][0]).toBe("attach-session");
|
||||
});
|
||||
|
||||
it("can skip attach", async () => {
|
||||
await spawnRelayTmux("echo hi", false, true);
|
||||
const spawnMock = spawn as unknown as vi.Mock;
|
||||
const hasAttach = spawnMock.mock.calls.some(
|
||||
(c) =>
|
||||
Array.isArray(c[1]) && (c[1] as string[]).includes("attach-session"),
|
||||
);
|
||||
expect(hasAttach).toBe(false);
|
||||
});
|
||||
});
|
||||
16
src/cli/wait.test.ts
Normal file
16
src/cli/wait.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { waitForever } from "./wait.js";
|
||||
|
||||
describe("waitForever", () => {
|
||||
it("creates an unref'ed interval and returns a pending promise", () => {
|
||||
const setIntervalSpy = vi.spyOn(global, "setInterval");
|
||||
const promise = waitForever();
|
||||
expect(setIntervalSpy).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
1_000_000,
|
||||
);
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
setIntervalSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user