pi-mono/packages/coding-agent/test/login-dialog-device-code.test.ts
Petr Houška 7151099d02 feat(coding-agent): confirm device-code browser and clipboard actions
Knowing how annoying re-logins in corporate environments can be, I think
we can open the browser and copy the code to the clipboard (what the user
asks for) without forcing it upon users (which I assume was the reason
it was removed in 8f0e9251b9).

This change adds optional browser opening and code copying during
device-code login after an additional confirmation (usually Enter).
It should work for all providers, with an optional opt-out controlled
by the provider author, not user settings.

Fixes #9282.
2026-10-22 21:38:58 +02:00

142 lines
5.9 KiB
TypeScript

import { setKeybindings, type TUI } from "@earendil-works/pi-tui";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { KeybindingsManager } from "../src/core/keybindings.ts";
import { LoginDialogComponent } from "../src/modes/interactive/components/login-dialog.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
import { stripAnsi } from "../src/utils/ansi.ts";
import { copyToClipboard } from "../src/utils/clipboard.ts";
import { openBrowser } from "../src/utils/open-browser.ts";
vi.mock("../src/utils/clipboard.ts", () => ({ copyToClipboard: vi.fn() }));
vi.mock("../src/utils/open-browser.ts", () => ({ openBrowser: vi.fn() }));
const device = { userCode: "ABCD-EFGH", verificationUri: "https://example.com/device" };
function createDialog() {
const onComplete = vi.fn();
const dialog = new LoginDialogComponent({ requestRender: vi.fn() } as unknown as TUI, "custom-provider", onComplete);
dialog.focused = true;
return { dialog, onComplete };
}
function render(dialog: LoginDialogComponent): string {
return stripAnsi(dialog.render(120).join("\n"));
}
// Regression #9282: device-code convenience actions require explicit confirmation.
describe("device-code login", () => {
beforeAll(() => initTheme("dark"));
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(copyToClipboard).mockResolvedValue(undefined);
setKeybindings(new KeybindingsManager());
});
it("opens the verification page and copies the code once after confirmation", async () => {
const { dialog } = createDialog();
const domain = dialog.showPrompt("GitHub Enterprise URL/domain (blank for github.com)");
dialog.handleInput("\r");
await expect(domain).resolves.toBe("");
dialog.showDeviceCode(device);
dialog.showWaiting("Waiting for authentication...");
expect(render(dialog)).toContain("to open browser and copy code");
expect(openBrowser).not.toHaveBeenCalled();
expect(copyToClipboard).not.toHaveBeenCalled();
dialog.handleInput("\r");
await Promise.resolve();
dialog.handleInput("\r");
expect(openBrowser).toHaveBeenCalledExactlyOnceWith(device.verificationUri);
expect(copyToClipboard).toHaveBeenCalledExactlyOnceWith(device.userCode);
expect(render(dialog)).toContain("Code copied to clipboard");
expect(render(dialog)).toContain(`Enter code: ${device.userCode}`);
expect(render(dialog)).toContain("Waiting for authentication...");
expect(render(dialog)).not.toContain("to open browser and copy code");
});
it.each([
[false, false],
[true, false],
[false, true],
] as const)(
"honors browser=%s / clipboard=%s provider overrides",
async (openBrowserOnConfirm, copyCodeOnConfirm) => {
const { dialog } = createDialog();
dialog.showDeviceCode({ ...device, openBrowserOnConfirm, copyCodeOnConfirm });
dialog.handleInput("\r");
await Promise.resolve();
expect(openBrowser).toHaveBeenCalledTimes(openBrowserOnConfirm ? 1 : 0);
expect(copyToClipboard).toHaveBeenCalledTimes(copyCodeOnConfirm ? 1 : 0);
expect(dialog.signal.aborted).toBe(false);
},
);
it("clears the previous auth URL copy action when showing a device code", () => {
const { dialog } = createDialog();
dialog.showAuth("https://example.com/authorize");
vi.mocked(openBrowser).mockClear();
dialog.showDeviceCode(device);
dialog.handleInput("\x18");
expect(copyToClipboard).not.toHaveBeenCalled();
expect(openBrowser).not.toHaveBeenCalled();
dialog.handleInput("\r");
expect(copyToClipboard).toHaveBeenCalledExactlyOnceWith(device.userCode);
});
it("clears device actions when switching to browser authentication", () => {
const { dialog } = createDialog();
dialog.showDeviceCode(device);
dialog.showAuth("https://example.com/authorize");
vi.mocked(openBrowser).mockClear();
dialog.handleInput("\r");
expect(copyToClipboard).not.toHaveBeenCalled();
expect(openBrowser).not.toHaveBeenCalled();
dialog.handleInput("\x18");
expect(copyToClipboard).toHaveBeenCalledExactlyOnceWith("https://example.com/authorize");
});
it("clears both action types when showing details", () => {
const { dialog } = createDialog();
dialog.showAuth("https://example.com/authorize");
dialog.showDetails(["Choose a login method"]);
dialog.handleInput("\x18");
expect(copyToClipboard).not.toHaveBeenCalled();
dialog.showDeviceCode(device);
dialog.showDetails(["Choose a login method"]);
vi.mocked(openBrowser).mockClear();
dialog.handleInput("\r");
expect(copyToClipboard).not.toHaveBeenCalled();
expect(openBrowser).not.toHaveBeenCalled();
});
it("cancels without opening a browser or changing the clipboard", () => {
const { dialog, onComplete } = createDialog();
dialog.showDeviceCode(device);
dialog.handleInput("\x1b");
dialog.handleInput("\r");
expect(dialog.signal.aborted).toBe(true);
expect(onComplete).toHaveBeenCalledExactlyOnceWith(false, "Login cancelled");
expect(openBrowser).not.toHaveBeenCalled();
expect(copyToClipboard).not.toHaveBeenCalled();
});
it("keeps manual login available when browser and clipboard access fail", async () => {
vi.mocked(openBrowser).mockImplementation(() => {
throw new Error("Browser unavailable");
});
vi.mocked(copyToClipboard).mockRejectedValue(new Error("Clipboard unavailable"));
const { dialog, onComplete } = createDialog();
dialog.showDeviceCode(device);
dialog.showWaiting("Waiting for authentication...");
expect(() => dialog.handleInput("\r")).not.toThrow();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(openBrowser).toHaveBeenCalledExactlyOnceWith(device.verificationUri);
expect(copyToClipboard).toHaveBeenCalledExactlyOnceWith(device.userCode);
expect(render(dialog)).toContain(device.verificationUri);
expect(render(dialog)).toContain(`Enter code: ${device.userCode}`);
expect(render(dialog)).toContain("Copy code manually");
expect(render(dialog)).toContain("Waiting for authentication...");
expect(dialog.signal.aborted).toBe(false);
expect(onComplete).not.toHaveBeenCalled();
});
});