|
1 | 1 | import { resolve } from "node:path"; |
| 2 | +import { fileURLToPath } from "node:url"; |
2 | 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
3 | 4 |
|
4 | | -const MOCK_BACKUP_DIR = resolve(process.cwd(), ".vitest-mock-backups"); |
| 5 | +const MOCK_BACKUP_DIR = fileURLToPath( |
| 6 | + new URL("./.vitest-mock-backups", import.meta.url), |
| 7 | +); |
5 | 8 | const mockBackupPath = (name: string): string => |
6 | 9 | resolve(MOCK_BACKUP_DIR, `${name}.json`); |
7 | 10 |
|
@@ -2899,6 +2902,55 @@ describe("codex manager cli commands", () => { |
2899 | 2902 | } |
2900 | 2903 | }); |
2901 | 2904 |
|
| 2905 | + it("propagates containment errors from batch backup assessment and returns to the login menu", async () => { |
| 2906 | + setInteractiveTTY(true); |
| 2907 | + loadAccountsMock.mockResolvedValue(null); |
| 2908 | + const now = Date.now(); |
| 2909 | + listNamedBackupsMock.mockResolvedValue([ |
| 2910 | + { |
| 2911 | + name: "escaped-backup", |
| 2912 | + path: mockBackupPath("escaped-backup"), |
| 2913 | + createdAt: null, |
| 2914 | + updatedAt: now, |
| 2915 | + sizeBytes: 128, |
| 2916 | + version: 3, |
| 2917 | + accountCount: 1, |
| 2918 | + schemaErrors: [], |
| 2919 | + valid: true, |
| 2920 | + loadError: undefined, |
| 2921 | + }, |
| 2922 | + ]); |
| 2923 | + assessNamedBackupRestoreMock.mockRejectedValueOnce( |
| 2924 | + new Error("Backup path escapes backup directory"), |
| 2925 | + ); |
| 2926 | + promptLoginModeMock |
| 2927 | + .mockResolvedValueOnce({ mode: "restore-backup" }) |
| 2928 | + .mockResolvedValueOnce({ mode: "cancel" }); |
| 2929 | + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); |
| 2930 | + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); |
| 2931 | + |
| 2932 | + try { |
| 2933 | + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); |
| 2934 | + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); |
| 2935 | + |
| 2936 | + expect(exitCode).toBe(0); |
| 2937 | + expect(promptLoginModeMock).toHaveBeenCalledTimes(2); |
| 2938 | + expect(selectMock).not.toHaveBeenCalled(); |
| 2939 | + expect(importAccountsMock).not.toHaveBeenCalled(); |
| 2940 | + expect(errorSpy).toHaveBeenCalledWith( |
| 2941 | + expect.stringContaining( |
| 2942 | + "Restore failed: Backup path escapes backup directory", |
| 2943 | + ), |
| 2944 | + ); |
| 2945 | + expect(warnSpy).not.toHaveBeenCalledWith( |
| 2946 | + expect.stringContaining('Skipped backup assessment for "escaped-backup"'), |
| 2947 | + ); |
| 2948 | + } finally { |
| 2949 | + errorSpy.mockRestore(); |
| 2950 | + warnSpy.mockRestore(); |
| 2951 | + } |
| 2952 | + }); |
| 2953 | + |
2902 | 2954 | it("keeps healthy backups selectable when one assessment fails", async () => { |
2903 | 2955 | setInteractiveTTY(true); |
2904 | 2956 | loadAccountsMock.mockResolvedValue(null); |
@@ -4845,6 +4897,96 @@ describe("codex manager cli commands", () => { |
4845 | 4897 | } |
4846 | 4898 | }); |
4847 | 4899 |
|
| 4900 | + it("waits for an in-flight menu quota refresh before starting backup restore", async () => { |
| 4901 | + setInteractiveTTY(true); |
| 4902 | + const now = Date.now(); |
| 4903 | + loadAccountsMock.mockResolvedValue({ |
| 4904 | + version: 3, |
| 4905 | + activeIndex: 0, |
| 4906 | + activeIndexByFamily: { codex: 0 }, |
| 4907 | + accounts: [ |
| 4908 | + { |
| 4909 | + email: "restore@example.com", |
| 4910 | + accountId: "acc-restore", |
| 4911 | + accessToken: "access-restore", |
| 4912 | + expiresAt: now + 3_600_000, |
| 4913 | + refreshToken: "refresh-restore", |
| 4914 | + addedAt: now, |
| 4915 | + lastUsed: now, |
| 4916 | + enabled: true, |
| 4917 | + }, |
| 4918 | + ], |
| 4919 | + }); |
| 4920 | + loadDashboardDisplaySettingsMock.mockResolvedValue({ |
| 4921 | + showPerAccountRows: true, |
| 4922 | + showQuotaDetails: true, |
| 4923 | + showForecastReasons: true, |
| 4924 | + showRecommendations: true, |
| 4925 | + showLiveProbeNotes: true, |
| 4926 | + menuAutoFetchLimits: true, |
| 4927 | + menuShowFetchStatus: true, |
| 4928 | + menuQuotaTtlMs: 60_000, |
| 4929 | + menuSortEnabled: true, |
| 4930 | + menuSortMode: "ready-first", |
| 4931 | + menuSortPinCurrent: true, |
| 4932 | + menuSortQuickSwitchVisibleRow: true, |
| 4933 | + }); |
| 4934 | + let currentQuotaCache: { |
| 4935 | + byAccountId: Record<string, unknown>; |
| 4936 | + byEmail: Record<string, unknown>; |
| 4937 | + } = { |
| 4938 | + byAccountId: {}, |
| 4939 | + byEmail: {}, |
| 4940 | + }; |
| 4941 | + loadQuotaCacheMock.mockImplementation(async () => |
| 4942 | + structuredClone(currentQuotaCache), |
| 4943 | + ); |
| 4944 | + saveQuotaCacheMock.mockImplementation(async (value: typeof currentQuotaCache) => { |
| 4945 | + currentQuotaCache = structuredClone(value); |
| 4946 | + }); |
| 4947 | + const fetchStarted = createDeferred<void>(); |
| 4948 | + const releaseFetch = createDeferred<void>(); |
| 4949 | + fetchCodexQuotaSnapshotMock.mockImplementation(async () => { |
| 4950 | + fetchStarted.resolve(); |
| 4951 | + await releaseFetch.promise; |
| 4952 | + return { |
| 4953 | + status: 200, |
| 4954 | + model: "gpt-5-codex", |
| 4955 | + primary: {}, |
| 4956 | + secondary: {}, |
| 4957 | + }; |
| 4958 | + }); |
| 4959 | + listNamedBackupsMock.mockResolvedValue([]); |
| 4960 | + promptLoginModeMock |
| 4961 | + .mockResolvedValueOnce({ mode: "restore-backup" }) |
| 4962 | + .mockResolvedValueOnce({ mode: "cancel" }); |
| 4963 | + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); |
| 4964 | + |
| 4965 | + try { |
| 4966 | + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); |
| 4967 | + const runPromise = runCodexMultiAuthCli(["auth", "login"]); |
| 4968 | + |
| 4969 | + await fetchStarted.promise; |
| 4970 | + await Promise.resolve(); |
| 4971 | + |
| 4972 | + expect(listNamedBackupsMock).not.toHaveBeenCalled(); |
| 4973 | + |
| 4974 | + releaseFetch.resolve(); |
| 4975 | + |
| 4976 | + const exitCode = await runPromise; |
| 4977 | + |
| 4978 | + expect(exitCode).toBe(0); |
| 4979 | + expect(saveQuotaCacheMock).toHaveBeenCalledTimes(1); |
| 4980 | + expect(listNamedBackupsMock).toHaveBeenCalledTimes(1); |
| 4981 | + expect(saveQuotaCacheMock.mock.invocationCallOrder[0]).toBeLessThan( |
| 4982 | + listNamedBackupsMock.mock.invocationCallOrder[0] ?? |
| 4983 | + Number.POSITIVE_INFINITY, |
| 4984 | + ); |
| 4985 | + } finally { |
| 4986 | + logSpy.mockRestore(); |
| 4987 | + } |
| 4988 | + }); |
| 4989 | + |
4848 | 4990 | it("skips a second destructive action while reset is already running", async () => { |
4849 | 4991 | const now = Date.now(); |
4850 | 4992 | const skipMessage = |
|
0 commit comments