Skip to content

Commit f07d870

Browse files
committed
fix(storage): pin snapshot retention to resolved path
1 parent 61d807a commit f07d870

3 files changed

Lines changed: 89 additions & 16 deletions

File tree

lib/destructive-actions.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
clearFlaggedAccounts,
88
findMatchingAccountIndex,
99
type FlaggedAccountStorageV1,
10-
getStoragePath,
1110
loadFlaggedAccounts,
1211
snapshotAccountStorage,
1312
snapshotAndClearAccounts,

lib/storage.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,10 +1187,6 @@ export function getFlaggedAccountsPath(): string {
11871187
return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME);
11881188
}
11891189

1190-
function getFlaggedAccountsPathForStoragePath(storagePath: string): string {
1191-
return join(dirname(storagePath), FLAGGED_ACCOUNTS_FILE_NAME);
1192-
}
1193-
11941190
function getLegacyFlaggedAccountsPath(): string {
11951191
return join(dirname(getStoragePath()), LEGACY_FLAGGED_ACCOUNTS_FILE_NAME);
11961192
}
@@ -2618,7 +2614,7 @@ export async function snapshotAccountStorage(
26182614
}
26192615

26202616
try {
2621-
await enforceSnapshotRetention(storagePath);
2617+
await enforceSnapshotRetention(resolvedStoragePath);
26222618
} catch (error) {
26232619
log.warn("Failed to enforce account snapshot retention", {
26242620
reason,
@@ -2919,7 +2915,7 @@ function assessNamedBackupRestoreCandidate(
29192915

29202916
export async function restoreNamedBackup(
29212917
name: string,
2922-
options: { assessment?: BackupRestoreAssessment } = {},
2918+
_options: { assessment?: BackupRestoreAssessment } = {},
29232919
): Promise<{ imported: number; total: number; skipped: number }> {
29242920
const backupPath = await resolveNamedBackupRestorePath(name);
29252921
const candidate = await loadImportableBackupCandidate(backupPath);

test/storage-recovery-paths.test.ts

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
snapshotAndClearAccounts,
1919
clearAccounts,
2020
getRestoreAssessment,
21+
withAccountAndFlaggedStorageTransaction,
2122
} from "../lib/storage.js";
2223
import {
2324
__resetSyncHistoryForTests,
@@ -989,11 +990,22 @@ describe("storage recovery paths", () => {
989990
});
990991
}
991992

992-
const backupsDir = getNamedBackupsDirectoryPath();
993-
const secondSnapshotName =
993+
const keptNames = (await listAccountSnapshots()).map((entry) => entry.name).sort();
994+
const expectedKeptNames = [
995+
firstSnapshot.name,
996+
"accounts-codex-cli-sync-snapshot-2026-03-16_00-00-02_000",
997+
"accounts-codex-cli-sync-snapshot-2026-03-16_00-00-03_000",
998+
"accounts-codex-cli-sync-snapshot-2026-03-16_00-00-04_000",
999+
].sort();
1000+
const expectedPrunedName =
9941001
"accounts-codex-cli-sync-snapshot-2026-03-16_00-00-01_000";
995-
expect(existsSync(firstSnapshot.path)).toBe(true);
996-
expect(existsSync(join(backupsDir, `${secondSnapshotName}.json`))).toBe(false);
1002+
const backupsDir = getNamedBackupsDirectoryPath();
1003+
1004+
expect(keptNames).toEqual(expectedKeptNames);
1005+
for (const name of expectedKeptNames) {
1006+
expect(existsSync(join(backupsDir, `${name}.json`))).toBe(true);
1007+
}
1008+
expect(existsSync(join(backupsDir, `${expectedPrunedName}.json`))).toBe(false);
9971009
});
9981010

9991011
it("retains rollback-referenced snapshots when a newer manual sync has no checkpoint", async () => {
@@ -1071,12 +1083,78 @@ describe("storage recovery paths", () => {
10711083

10721084
const result = await pruneAutoGeneratedSnapshots();
10731085
const backupsDir = getNamedBackupsDirectoryPath();
1074-
const secondSnapshotName =
1075-
"accounts-codex-cli-sync-snapshot-2026-03-16_01-00-01_000";
1086+
const expectedKeptNames = [
1087+
firstSnapshot.name,
1088+
"accounts-codex-cli-sync-snapshot-2026-03-16_01-00-02_000",
1089+
"accounts-codex-cli-sync-snapshot-2026-03-16_01-00-03_000",
1090+
"accounts-codex-cli-sync-snapshot-2026-03-16_01-00-04_000",
1091+
].sort();
1092+
const expectedAlreadyPrunedNames = [
1093+
"accounts-codex-cli-sync-snapshot-2026-03-16_01-00-01_000",
1094+
];
1095+
1096+
expect(result.kept.map((entry) => entry.name).sort()).toEqual(expectedKeptNames);
1097+
expect(result.pruned).toEqual([]);
1098+
for (const name of expectedKeptNames) {
1099+
expect(existsSync(join(backupsDir, `${name}.json`))).toBe(true);
1100+
}
1101+
for (const name of expectedAlreadyPrunedNames) {
1102+
expect(existsSync(join(backupsDir, `${name}.json`))).toBe(false);
1103+
}
1104+
});
1105+
1106+
it("enforces snapshot retention against the transaction-pinned storage path", async () => {
1107+
const primaryDir = join(workDir, "primary");
1108+
const alternateDir = join(workDir, "alternate");
1109+
const primaryStoragePath = join(primaryDir, "openai-codex-accounts.json");
1110+
const alternateStoragePath = join(alternateDir, "openai-codex-accounts.json");
1111+
await fs.mkdir(primaryDir, { recursive: true });
1112+
await fs.mkdir(alternateDir, { recursive: true });
1113+
setStoragePathDirect(primaryStoragePath);
1114+
await saveAccounts({
1115+
version: 3,
1116+
activeIndex: 0,
1117+
accounts: [
1118+
{
1119+
refreshToken: "tx-retention-refresh",
1120+
accountId: "tx-retention-account",
1121+
addedAt: 1,
1122+
lastUsed: 1,
1123+
},
1124+
],
1125+
});
1126+
1127+
const baseTime = Date.UTC(2026, 2, 16, 3, 0, 0, 0);
1128+
for (let index = 0; index < 3; index += 1) {
1129+
await snapshotAccountStorage({
1130+
reason: "codex-cli-sync",
1131+
now: baseTime + index * 1_000,
1132+
});
1133+
}
1134+
1135+
const primaryBackupsDir = getNamedBackupsDirectoryPath();
1136+
await withAccountAndFlaggedStorageTransaction(async (current) => {
1137+
setStoragePathDirect(alternateStoragePath);
1138+
await snapshotAccountStorage({
1139+
reason: "codex-cli-sync",
1140+
now: baseTime + 3_000,
1141+
storage: current,
1142+
});
1143+
});
10761144

1077-
expect(result.kept.map((entry) => entry.name)).toContain(firstSnapshot.name);
1078-
expect(existsSync(firstSnapshot.path)).toBe(true);
1079-
expect(existsSync(join(backupsDir, `${secondSnapshotName}.json`))).toBe(false);
1145+
const primarySnapshotNames = (await fs.readdir(primaryBackupsDir))
1146+
.filter((entry) => entry.endsWith(".json"))
1147+
.map((entry) => entry.slice(0, -".json".length))
1148+
.sort();
1149+
const expectedPrimaryNames = [
1150+
"accounts-codex-cli-sync-snapshot-2026-03-16_03-00-01_000",
1151+
"accounts-codex-cli-sync-snapshot-2026-03-16_03-00-02_000",
1152+
"accounts-codex-cli-sync-snapshot-2026-03-16_03-00-03_000",
1153+
].sort();
1154+
1155+
expect(primarySnapshotNames).toEqual(expectedPrimaryNames);
1156+
expect(existsSync(primaryStoragePath)).toBe(true);
1157+
expect(existsSync(alternateStoragePath)).toBe(false);
10801158
});
10811159

10821160
it("falls back to the newest live rollback snapshot when a newer recorded checkpoint file is missing", async () => {

0 commit comments

Comments
 (0)