|
| 1 | +import { existsSync, promises as fs } from "node:fs"; |
| 2 | +import { tmpdir } from "node:os"; |
| 3 | +import { join } from "node:path"; |
| 4 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 5 | +import { clearCodexCliStateCache } from "../lib/codex-cli/state.js"; |
| 6 | +import type { AccountStorageV3 } from "../lib/storage.js"; |
| 7 | +import { removeWithRetry } from "./helpers/remove-with-retry.js"; |
| 8 | + |
| 9 | +describe("release-main-prs regressions", () => { |
| 10 | + let tempDir: string; |
| 11 | + let accountsPath: string; |
| 12 | + let authPath: string; |
| 13 | + let configPath: string; |
| 14 | + let previousMultiAuthDir: string | undefined; |
| 15 | + let previousAccountsPath: string | undefined; |
| 16 | + let previousAuthPath: string | undefined; |
| 17 | + let previousConfigPath: string | undefined; |
| 18 | + let previousSync: string | undefined; |
| 19 | + let previousEnforceFileStore: string | undefined; |
| 20 | + |
| 21 | + beforeEach(async () => { |
| 22 | + previousMultiAuthDir = process.env.CODEX_MULTI_AUTH_DIR; |
| 23 | + previousAccountsPath = process.env.CODEX_CLI_ACCOUNTS_PATH; |
| 24 | + previousAuthPath = process.env.CODEX_CLI_AUTH_PATH; |
| 25 | + previousConfigPath = process.env.CODEX_CLI_CONFIG_PATH; |
| 26 | + previousSync = process.env.CODEX_MULTI_AUTH_SYNC_CODEX_CLI; |
| 27 | + previousEnforceFileStore = |
| 28 | + process.env.CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE; |
| 29 | + |
| 30 | + tempDir = await fs.mkdtemp(join(tmpdir(), "codex-release-main-prs-")); |
| 31 | + accountsPath = join(tempDir, "accounts.json"); |
| 32 | + authPath = join(tempDir, "auth.json"); |
| 33 | + configPath = join(tempDir, "config.toml"); |
| 34 | + |
| 35 | + process.env.CODEX_MULTI_AUTH_DIR = tempDir; |
| 36 | + process.env.CODEX_CLI_ACCOUNTS_PATH = accountsPath; |
| 37 | + process.env.CODEX_CLI_AUTH_PATH = authPath; |
| 38 | + process.env.CODEX_CLI_CONFIG_PATH = configPath; |
| 39 | + process.env.CODEX_MULTI_AUTH_SYNC_CODEX_CLI = "1"; |
| 40 | + process.env.CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE = "1"; |
| 41 | + |
| 42 | + vi.resetModules(); |
| 43 | + clearCodexCliStateCache(); |
| 44 | + }); |
| 45 | + |
| 46 | + afterEach(async () => { |
| 47 | + vi.restoreAllMocks(); |
| 48 | + clearCodexCliStateCache(); |
| 49 | + |
| 50 | + const { setStoragePathDirect } = await import("../lib/storage.js"); |
| 51 | + setStoragePathDirect(null); |
| 52 | + |
| 53 | + if (previousMultiAuthDir === undefined) delete process.env.CODEX_MULTI_AUTH_DIR; |
| 54 | + else process.env.CODEX_MULTI_AUTH_DIR = previousMultiAuthDir; |
| 55 | + if (previousAccountsPath === undefined) delete process.env.CODEX_CLI_ACCOUNTS_PATH; |
| 56 | + else process.env.CODEX_CLI_ACCOUNTS_PATH = previousAccountsPath; |
| 57 | + if (previousAuthPath === undefined) delete process.env.CODEX_CLI_AUTH_PATH; |
| 58 | + else process.env.CODEX_CLI_AUTH_PATH = previousAuthPath; |
| 59 | + if (previousConfigPath === undefined) delete process.env.CODEX_CLI_CONFIG_PATH; |
| 60 | + else process.env.CODEX_CLI_CONFIG_PATH = previousConfigPath; |
| 61 | + if (previousSync === undefined) delete process.env.CODEX_MULTI_AUTH_SYNC_CODEX_CLI; |
| 62 | + else process.env.CODEX_MULTI_AUTH_SYNC_CODEX_CLI = previousSync; |
| 63 | + if (previousEnforceFileStore === undefined) { |
| 64 | + delete process.env.CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE; |
| 65 | + } else { |
| 66 | + process.env.CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE = previousEnforceFileStore; |
| 67 | + } |
| 68 | + |
| 69 | + await removeWithRetry(tempDir, { recursive: true, force: true }); |
| 70 | + }); |
| 71 | + |
| 72 | + it("keeps canonical storage and unified settings isolated from Codex CLI mirror files", async () => { |
| 73 | + const { |
| 74 | + saveUnifiedPluginConfig, |
| 75 | + saveUnifiedDashboardSettings, |
| 76 | + loadUnifiedPluginConfigSync, |
| 77 | + loadUnifiedDashboardSettings, |
| 78 | + } = await import("../lib/unified-settings.js"); |
| 79 | + const { syncAccountStorageFromCodexCli } = await import("../lib/codex-cli/sync.js"); |
| 80 | + const { MODEL_FAMILIES } = await import("../lib/prompts/codex.js"); |
| 81 | + |
| 82 | + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 90_000 }); |
| 83 | + await saveUnifiedDashboardSettings({ |
| 84 | + menuShowLastUsed: false, |
| 85 | + uiThemePreset: "green", |
| 86 | + }); |
| 87 | + |
| 88 | + await fs.writeFile( |
| 89 | + accountsPath, |
| 90 | + JSON.stringify( |
| 91 | + { |
| 92 | + activeAccountId: "acc_mirror", |
| 93 | + accounts: [ |
| 94 | + { |
| 95 | + accountId: "acc_mirror", |
| 96 | + email: "mirror@example.com", |
| 97 | + auth: { |
| 98 | + tokens: { |
| 99 | + access_token: "mirror-access", |
| 100 | + refresh_token: "mirror-refresh", |
| 101 | + }, |
| 102 | + }, |
| 103 | + }, |
| 104 | + ], |
| 105 | + }, |
| 106 | + null, |
| 107 | + 2, |
| 108 | + ), |
| 109 | + "utf-8", |
| 110 | + ); |
| 111 | + |
| 112 | + const current: AccountStorageV3 = { |
| 113 | + version: 3, |
| 114 | + accounts: [ |
| 115 | + { |
| 116 | + accountId: "acc_canonical", |
| 117 | + email: "canonical@example.com", |
| 118 | + refreshToken: "canonical-refresh", |
| 119 | + addedAt: 1, |
| 120 | + lastUsed: 1, |
| 121 | + }, |
| 122 | + ], |
| 123 | + activeIndex: 0, |
| 124 | + activeIndexByFamily: Object.fromEntries(MODEL_FAMILIES.map((family) => [family, 0])), |
| 125 | + }; |
| 126 | + |
| 127 | + const result = await syncAccountStorageFromCodexCli(current); |
| 128 | + |
| 129 | + expect(result.changed).toBe(false); |
| 130 | + expect(result.storage).toBe(current); |
| 131 | + expect(result.storage?.accounts).toEqual(current.accounts); |
| 132 | + expect(loadUnifiedPluginConfigSync()).toEqual({ |
| 133 | + codexMode: true, |
| 134 | + fetchTimeoutMs: 90_000, |
| 135 | + }); |
| 136 | + expect(await loadUnifiedDashboardSettings()).toEqual({ |
| 137 | + menuShowLastUsed: false, |
| 138 | + uiThemePreset: "green", |
| 139 | + }); |
| 140 | + }); |
| 141 | + |
| 142 | + it("keeps flagged reset suppression active even when Codex CLI mirrors exist", async () => { |
| 143 | + const { |
| 144 | + clearFlaggedAccounts, |
| 145 | + getFlaggedAccountsPath, |
| 146 | + loadFlaggedAccounts, |
| 147 | + saveFlaggedAccounts, |
| 148 | + setStoragePathDirect, |
| 149 | + } = await import("../lib/storage.js"); |
| 150 | + const { syncAccountStorageFromCodexCli } = await import("../lib/codex-cli/sync.js"); |
| 151 | + |
| 152 | + const storagePath = join(tempDir, "canonical-accounts.json"); |
| 153 | + setStoragePathDirect(storagePath); |
| 154 | + |
| 155 | + await saveFlaggedAccounts({ |
| 156 | + version: 1, |
| 157 | + accounts: [ |
| 158 | + { |
| 159 | + refreshToken: "stale-primary", |
| 160 | + flaggedAt: 1, |
| 161 | + addedAt: 1, |
| 162 | + lastUsed: 1, |
| 163 | + }, |
| 164 | + ], |
| 165 | + }); |
| 166 | + |
| 167 | + await fs.writeFile( |
| 168 | + accountsPath, |
| 169 | + JSON.stringify( |
| 170 | + { |
| 171 | + activeAccountId: "acc_mirror", |
| 172 | + accounts: [ |
| 173 | + { |
| 174 | + accountId: "acc_mirror", |
| 175 | + email: "mirror@example.com", |
| 176 | + auth: { |
| 177 | + tokens: { |
| 178 | + access_token: "mirror-access", |
| 179 | + refresh_token: "mirror-refresh", |
| 180 | + }, |
| 181 | + }, |
| 182 | + }, |
| 183 | + ], |
| 184 | + }, |
| 185 | + null, |
| 186 | + 2, |
| 187 | + ), |
| 188 | + "utf-8", |
| 189 | + ); |
| 190 | + |
| 191 | + const flaggedPath = getFlaggedAccountsPath(); |
| 192 | + const originalUnlink = fs.unlink.bind(fs); |
| 193 | + const unlinkSpy = vi.spyOn(fs, "unlink").mockImplementation(async (targetPath) => { |
| 194 | + if (targetPath === flaggedPath) { |
| 195 | + const error = new Error("EPERM primary delete") as NodeJS.ErrnoException; |
| 196 | + error.code = "EPERM"; |
| 197 | + throw error; |
| 198 | + } |
| 199 | + return originalUnlink(targetPath); |
| 200 | + }); |
| 201 | + |
| 202 | + await expect(clearFlaggedAccounts()).rejects.toThrow("EPERM primary delete"); |
| 203 | + |
| 204 | + const flagged = await loadFlaggedAccounts(); |
| 205 | + const syncResult = await syncAccountStorageFromCodexCli(null); |
| 206 | + |
| 207 | + expect(existsSync(flaggedPath)).toBe(true); |
| 208 | + expect(flagged.accounts).toHaveLength(0); |
| 209 | + expect(syncResult.changed).toBe(false); |
| 210 | + expect(syncResult.storage).toBeNull(); |
| 211 | + |
| 212 | + unlinkSpy.mockRestore(); |
| 213 | + }); |
| 214 | +}); |
0 commit comments