Skip to content

Commit 428d3a6

Browse files
authored
Merge pull request #321 from ndycode/fix/main-cli-test-regressions
test: harden codex manager cli isolation
2 parents e3f5dfc + ebbaf46 commit 428d3a6

File tree

2 files changed

+119
-2
lines changed

2 files changed

+119
-2
lines changed

test/codex-manager-cli.test.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,14 @@ const stdinIsTTYDescriptor = Object.getOwnPropertyDescriptor(
247247
process.stdin,
248248
"isTTY",
249249
);
250+
const stdinReadableEndedDescriptor = Object.getOwnPropertyDescriptor(
251+
process.stdin,
252+
"readableEnded",
253+
);
254+
const stdinDestroyedDescriptor = Object.getOwnPropertyDescriptor(
255+
process.stdin,
256+
"destroyed",
257+
);
250258
const stdoutIsTTYDescriptor = Object.getOwnPropertyDescriptor(
251259
process.stdout,
252260
"isTTY",
@@ -269,13 +277,38 @@ function restoreTTYDescriptors(): void {
269277
} else {
270278
delete (process.stdin as unknown as { isTTY?: boolean }).isTTY;
271279
}
280+
if (stdinReadableEndedDescriptor) {
281+
Object.defineProperty(
282+
process.stdin,
283+
"readableEnded",
284+
stdinReadableEndedDescriptor,
285+
);
286+
} else {
287+
delete (process.stdin as unknown as { readableEnded?: boolean }).readableEnded;
288+
}
289+
if (stdinDestroyedDescriptor) {
290+
Object.defineProperty(process.stdin, "destroyed", stdinDestroyedDescriptor);
291+
} else {
292+
delete (process.stdin as unknown as { destroyed?: boolean }).destroyed;
293+
}
272294
if (stdoutIsTTYDescriptor) {
273295
Object.defineProperty(process.stdout, "isTTY", stdoutIsTTYDescriptor);
274296
} else {
275297
delete (process.stdout as unknown as { isTTY?: boolean }).isTTY;
276298
}
277299
}
278300

301+
function setOpenStdinState(): void {
302+
Object.defineProperty(process.stdin, "readableEnded", {
303+
value: false,
304+
configurable: true,
305+
});
306+
Object.defineProperty(process.stdin, "destroyed", {
307+
value: false,
308+
configurable: true,
309+
});
310+
}
311+
279312
function createDeferred<T>(): {
280313
promise: Promise<T>;
281314
resolve: (value: T | PromiseLike<T>) => void;
@@ -557,7 +590,7 @@ function readSettingsHubPanelContract(): string[] {
557590
}
558591

559592
describe("codex manager cli commands", () => {
560-
beforeEach(() => {
593+
beforeEach(async () => {
561594
vi.resetModules();
562595
vi.clearAllMocks();
563596
loadAccountsMock.mockReset();
@@ -574,6 +607,7 @@ describe("codex manager cli commands", () => {
574607
loadCodexCliStateMock.mockReset();
575608
promptAddAnotherAccountMock.mockReset();
576609
promptLoginModeMock.mockReset();
610+
promptQuestionMock.mockReset();
577611
fetchCodexQuotaSnapshotMock.mockReset();
578612
loadDashboardDisplaySettingsMock.mockReset();
579613
saveDashboardDisplaySettingsMock.mockReset();
@@ -583,6 +617,12 @@ describe("codex manager cli commands", () => {
583617
savePluginConfigMock.mockReset();
584618
selectMock.mockReset();
585619
confirmMock.mockReset();
620+
planOcChatgptSyncMock.mockReset();
621+
applyOcChatgptSyncMock.mockReset();
622+
runNamedBackupExportMock.mockReset();
623+
exportNamedBackupMock.mockReset();
624+
detectOcChatgptMultiAuthTargetMock.mockReset();
625+
normalizeAccountStorageMock.mockReset();
586626
confirmMock.mockResolvedValue(true);
587627
fetchCodexQuotaSnapshotMock.mockResolvedValue({
588628
status: 200,
@@ -727,8 +767,35 @@ describe("codex manager cli commands", () => {
727767
selectMock.mockResolvedValue(undefined);
728768
getNamedBackupsMock.mockResolvedValue([]);
729769
restoreTTYDescriptors();
770+
setOpenStdinState();
730771
setStoragePathMock.mockReset();
731772
getStoragePathMock.mockReturnValue("/mock/openai-codex-accounts.json");
773+
normalizeAccountStorageMock.mockImplementation((value) => value);
774+
775+
const authModule = await import("../lib/auth/auth.js");
776+
vi.mocked(authModule.createAuthorizationFlow).mockReset();
777+
vi.mocked(authModule.exchangeAuthorizationCode).mockReset();
778+
vi.mocked(authModule.parseAuthorizationInput).mockReset();
779+
vi.mocked(authModule.parseAuthorizationInput).mockImplementation(
780+
(input: string) => {
781+
const codeMatch = input.match(/code=([^&]+)/);
782+
const stateMatch = input.match(/state=([^&#]+)/);
783+
return {
784+
code: codeMatch?.[1],
785+
state: stateMatch?.[1],
786+
};
787+
},
788+
);
789+
790+
const browserModule = await import("../lib/auth/browser.js");
791+
vi.mocked(browserModule.isBrowserLaunchSuppressed).mockReset();
792+
vi.mocked(browserModule.isBrowserLaunchSuppressed).mockReturnValue(false);
793+
vi.mocked(browserModule.openBrowserUrl).mockReset();
794+
vi.mocked(browserModule.copyTextToClipboard).mockReset();
795+
vi.mocked(browserModule.copyTextToClipboard).mockReturnValue(true);
796+
797+
const serverModule = await import("../lib/auth/server.js");
798+
vi.mocked(serverModule.startLocalOAuthServer).mockReset();
732799
});
733800

734801
afterEach(() => {
@@ -5350,6 +5417,54 @@ describe("codex manager cli commands", () => {
53505417
expect(storageState.accounts).toHaveLength(0);
53515418
});
53525419

5420+
it("skips manual callback prompting when stdin is already closed in non-tty mode", async () => {
5421+
setInteractiveTTY(false);
5422+
setOpenStdinState();
5423+
Object.defineProperty(process.stdin, "readableEnded", {
5424+
value: true,
5425+
configurable: true,
5426+
});
5427+
let storageState = {
5428+
version: 3 as const,
5429+
activeIndex: 0,
5430+
activeIndexByFamily: { codex: 0 },
5431+
accounts: [] as Array<Record<string, unknown>>,
5432+
};
5433+
loadAccountsMock.mockImplementation(async () =>
5434+
structuredClone(storageState),
5435+
);
5436+
saveAccountsMock.mockImplementation(async (nextStorage) => {
5437+
storageState = structuredClone(nextStorage);
5438+
});
5439+
promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" });
5440+
5441+
const authModule = await import("../lib/auth/auth.js");
5442+
vi.mocked(authModule.createAuthorizationFlow).mockResolvedValueOnce({
5443+
pkce: { challenge: "pkce-challenge", verifier: "pkce-verifier" },
5444+
state: "oauth-state",
5445+
url: "https://auth.openai.com/mock",
5446+
});
5447+
const exchangeAuthorizationCodeMock = vi.mocked(
5448+
authModule.exchangeAuthorizationCode,
5449+
);
5450+
5451+
const browserModule = await import("../lib/auth/browser.js");
5452+
const openBrowserUrlMock = vi.mocked(browserModule.openBrowserUrl);
5453+
const serverModule = await import("../lib/auth/server.js");
5454+
5455+
const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
5456+
const exitCode = await runCodexMultiAuthCli(["auth", "login", "--manual"]);
5457+
5458+
expect(exitCode).toBe(0);
5459+
expect(promptQuestionMock).not.toHaveBeenCalled();
5460+
expect(openBrowserUrlMock).not.toHaveBeenCalled();
5461+
expect(
5462+
vi.mocked(serverModule.startLocalOAuthServer),
5463+
).not.toHaveBeenCalled();
5464+
expect(exchangeAuthorizationCodeMock).not.toHaveBeenCalled();
5465+
expect(storageState.accounts).toHaveLength(0);
5466+
});
5467+
53535468
it("falls back to pasted manual input when Windows-style callback bind fails", async () => {
53545469
setInteractiveTTY(false);
53555470
const now = Date.now();

test/wait-utils.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ describe("wait utils", () => {
3030

3131
it("shows countdown toasts and sleeps in intervals", async () => {
3232
const showToast = vi.fn(async () => undefined);
33-
const sleep = vi.fn(async () => undefined);
33+
const sleep = vi.fn(async (ms: number) => {
34+
await vi.advanceTimersByTimeAsync(ms);
35+
});
3436
await sleepWithCountdown({
3537
totalMs: 10_000,
3638
message: "Waiting",

0 commit comments

Comments
 (0)