mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
pi auth login <provider> logs in from the shell without starting a session, like pi mcp login. Account sign-in is the default and takes the provider's browser method; when the provider has another method, it prints a hint to rerun with --no-browser. --no-browser skips the browser method and uses the other one, such as device code, asking only when there are several. --api-key saves an API key instead and cannot be combined with --no-browser. After logging in, it fetches the provider's model catalog, and for Radius it points the Radius MCP server in the global mcp.json at the login without asking. pi auth logout <provider> removes the stored credentials like /logout and succeeds when nothing is stored. For MCP servers that use provider credentials, pi mcp login and logout now refer to these commands instead of /login and /logout. /login shares the check for the Radius MCP server entry and the web URL helpers with the new command.
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import { PassThrough } from "node:stream";
|
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import { createProcessLoginTerminal } from "../src/cli/auth-terminal.ts";
|
|
|
|
const inputs: PassThrough[] = [];
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const input of inputs) input.destroy();
|
|
inputs.length = 0;
|
|
});
|
|
|
|
function setup() {
|
|
const input = new PassThrough();
|
|
inputs.push(input);
|
|
// Readline only needs the readable stream; the test does not exercise TTY detection or raw mode.
|
|
vi.spyOn(process, "stdin", "get").mockReturnValue(input as unknown as typeof process.stdin);
|
|
vi.spyOn(process.stdout, "write").mockReturnValue(true);
|
|
const controller = new AbortController();
|
|
const interrupt = vi.fn(() => controller.abort());
|
|
const terminal = createProcessLoginTerminal(interrupt);
|
|
return { input, controller, interrupt, terminal };
|
|
}
|
|
|
|
describe("auth terminal questions", () => {
|
|
test("rejects and cancels the login when stdin closes before an answer", async () => {
|
|
const { input, controller, interrupt, terminal } = setup();
|
|
const answer = terminal.question("Enter domain: ", controller.signal);
|
|
const rejected = expect(answer).rejects.toMatchObject({ name: "AbortError" });
|
|
input.end();
|
|
await rejected;
|
|
expect(interrupt).toHaveBeenCalledOnce();
|
|
expect(controller.signal.aborted).toBe(true);
|
|
});
|
|
|
|
test("normal cleanup does not cancel the login after an answer", async () => {
|
|
const { input, controller, interrupt, terminal } = setup();
|
|
const answer = terminal.question("Enter domain: ", controller.signal);
|
|
input.write("example.com\n");
|
|
await expect(answer).resolves.toBe("example.com");
|
|
expect(interrupt).not.toHaveBeenCalled();
|
|
expect(controller.signal.aborted).toBe(false);
|
|
});
|
|
|
|
test("keeps an answer delivered immediately before EOF", async () => {
|
|
const { input, controller, interrupt, terminal } = setup();
|
|
const answer = terminal.question("Enter domain: ", controller.signal);
|
|
input.end("example.com\n");
|
|
await expect(answer).resolves.toBe("example.com");
|
|
expect(interrupt).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("prompt cancellation does not cancel the whole login", async () => {
|
|
const { controller, interrupt, terminal } = setup();
|
|
const prompt = new AbortController();
|
|
const answer = terminal.question("Paste code: ", AbortSignal.any([controller.signal, prompt.signal]));
|
|
const rejected = expect(answer).rejects.toMatchObject({ name: "AbortError" });
|
|
prompt.abort();
|
|
await rejected;
|
|
expect(interrupt).not.toHaveBeenCalled();
|
|
expect(controller.signal.aborted).toBe(false);
|
|
});
|
|
});
|