diff --git a/lib/storage.ts b/lib/storage.ts index b8a9ea56..cb7d705f 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -23,6 +23,16 @@ export { } from "./storage/identity.js"; import { normalizeEmailKey } from "./storage/identity.js"; +import { + ACCOUNTS_BACKUP_SUFFIX, + ACCOUNTS_WAL_SUFFIX, + getFlaggedAccountsPath as buildFlaggedAccountsPath, + getAccountsBackupPath, + getAccountsBackupRecoveryCandidates, + getAccountsWalPath, + getIntentionalResetMarkerPath, + RESET_MARKER_SUFFIX, +} from "./storage/file-paths.js"; import { type AccountMetadataV1, type AccountMetadataV3, @@ -54,12 +64,8 @@ const log = createLogger("storage"); const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json"; const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json"; const LEGACY_FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-blocked-accounts.json"; -const ACCOUNTS_BACKUP_SUFFIX = ".bak"; -const ACCOUNTS_WAL_SUFFIX = ".wal"; -const ACCOUNTS_BACKUP_HISTORY_DEPTH = 3; const BACKUP_COPY_MAX_ATTEMPTS = 5; const BACKUP_COPY_BASE_DELAY_MS = 10; -const RESET_MARKER_SUFFIX = ".reset-intent"; let storageBackupEnabled = true; let lastAccountsSaveTimestamp = 0; @@ -367,25 +373,6 @@ export function setStorageBackupEnabled(enabled: boolean): void { storageBackupEnabled = enabled; } -function getAccountsBackupPath(path: string): string { - return `${path}${ACCOUNTS_BACKUP_SUFFIX}`; -} - -function getAccountsBackupPathAtIndex(path: string, index: number): string { - if (index <= 0) { - return getAccountsBackupPath(path); - } - return `${path}${ACCOUNTS_BACKUP_SUFFIX}.${index}`; -} - -function getAccountsBackupRecoveryCandidates(path: string): string[] { - const candidates: string[] = []; - for (let i = 0; i < ACCOUNTS_BACKUP_HISTORY_DEPTH; i += 1) { - candidates.push(getAccountsBackupPathAtIndex(path, i)); - } - return candidates; -} - async function getAccountsBackupRecoveryCandidatesWithDiscovery( path: string, ): Promise { @@ -425,10 +412,6 @@ async function getAccountsBackupRecoveryCandidatesWithDiscovery( return [...knownCandidates, ...discoveredOrdered]; } -function getAccountsWalPath(path: string): string { - return `${path}${ACCOUNTS_WAL_SUFFIX}`; -} - async function copyFileWithRetry( sourcePath: string, destinationPath: string, @@ -605,10 +588,6 @@ function computeSha256(value: string): string { return createHash("sha256").update(value).digest("hex"); } -function getIntentionalResetMarkerPath(path: string): string { - return `${path}${RESET_MARKER_SUFFIX}`; -} - function createEmptyStorageWithMetadata( restoreEligible: boolean, restoreReason: RestoreReason, @@ -996,11 +975,14 @@ export async function exportNamedBackup( } export function getFlaggedAccountsPath(): string { - return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME); + return buildFlaggedAccountsPath(getStoragePath(), FLAGGED_ACCOUNTS_FILE_NAME); } function getLegacyFlaggedAccountsPath(): string { - return join(dirname(getStoragePath()), LEGACY_FLAGGED_ACCOUNTS_FILE_NAME); + return buildFlaggedAccountsPath( + getStoragePath(), + LEGACY_FLAGGED_ACCOUNTS_FILE_NAME, + ); } async function migrateLegacyProjectStorageIfNeeded( diff --git a/lib/storage/file-paths.ts b/lib/storage/file-paths.ts new file mode 100644 index 00000000..e151afe7 --- /dev/null +++ b/lib/storage/file-paths.ts @@ -0,0 +1,42 @@ +import { dirname, join } from "node:path"; + +export const ACCOUNTS_BACKUP_SUFFIX = ".bak"; +export const ACCOUNTS_WAL_SUFFIX = ".wal"; +const ACCOUNTS_BACKUP_HISTORY_DEPTH = 3; +export const RESET_MARKER_SUFFIX = ".reset-intent"; + +export function getAccountsBackupPath(path: string): string { + return `${path}${ACCOUNTS_BACKUP_SUFFIX}`; +} + +function getAccountsBackupPathAtIndex(path: string, index: number): string { + if (index <= 0) return getAccountsBackupPath(path); + return `${path}${ACCOUNTS_BACKUP_SUFFIX}.${index}`; +} + +export function getAccountsBackupRecoveryCandidates(path: string): string[] { + const candidates: string[] = []; + for (let i = 0; i < ACCOUNTS_BACKUP_HISTORY_DEPTH; i += 1) { + candidates.push(getAccountsBackupPathAtIndex(path, i)); + } + return candidates; +} + +export function getAccountsWalPath(path: string): string { + return `${path}${ACCOUNTS_WAL_SUFFIX}`; +} + +export function getIntentionalResetMarkerPath(path: string): string { + return `${path}${RESET_MARKER_SUFFIX}`; +} + +export function getFlaggedAccountsPath( + storagePath: string, + fileName: string, +): string { + return buildSiblingStoragePath(storagePath, fileName); +} + +function buildSiblingStoragePath(storagePath: string, fileName: string): string { + return join(dirname(storagePath), fileName); +} diff --git a/test/storage-file-paths.test.ts b/test/storage-file-paths.test.ts new file mode 100644 index 00000000..9a9cda23 --- /dev/null +++ b/test/storage-file-paths.test.ts @@ -0,0 +1,56 @@ +import { dirname, join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + ACCOUNTS_BACKUP_SUFFIX, + ACCOUNTS_WAL_SUFFIX, + getAccountsBackupPath, + getAccountsBackupRecoveryCandidates, + getAccountsWalPath, + getFlaggedAccountsPath, + getIntentionalResetMarkerPath, + RESET_MARKER_SUFFIX, +} from "../lib/storage/file-paths.js"; + +describe("storage file paths", () => { + it("builds the primary backup, wal, and reset marker paths", () => { + const storagePath = "/tmp/openai-codex-accounts.json"; + + expect(getAccountsBackupPath(storagePath)).toBe( + `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`, + ); + expect(getAccountsWalPath(storagePath)).toBe( + `${storagePath}${ACCOUNTS_WAL_SUFFIX}`, + ); + expect(getIntentionalResetMarkerPath(storagePath)).toBe( + `${storagePath}${RESET_MARKER_SUFFIX}`, + ); + }); + + it("returns backup recovery candidates for the base backup and history slots", () => { + const storagePath = "/tmp/openai-codex-accounts.json"; + + expect(getAccountsBackupRecoveryCandidates(storagePath)).toEqual([ + `${storagePath}.bak`, + `${storagePath}.bak.1`, + `${storagePath}.bak.2`, + ]); + }); + + it("builds flagged storage paths next to the active storage file", () => { + const storagePath = "/tmp/config/openai-codex-accounts.json"; + const fileName = "openai-codex-flagged-accounts.json"; + + expect(getFlaggedAccountsPath(storagePath, fileName)).toBe( + join(dirname(storagePath), fileName), + ); + }); + + it("uses dirname/join semantics consistently for windows-like storage paths", () => { + const storagePath = String.raw`C:\Users\user\.codex\openai-codex-accounts.json`; + const fileName = "openai-codex-blocked-accounts.json"; + + expect(getFlaggedAccountsPath(storagePath, fileName)).toBe( + join(dirname(storagePath), fileName), + ); + }); +});