Skip to content

Commit 2b272c5

Browse files
committed
Fix rollback checkpoint selection
1 parent d262177 commit 2b272c5

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

lib/codex-cli/sync.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,19 +372,30 @@ function isManualChangedSyncRun(run: CodexCliSyncRun | null): run is CodexCliSyn
372372
return Boolean(run && run.outcome === "changed" && run.trigger === "manual");
373373
}
374374

375+
function hasUsableRollbackSnapshot(
376+
snapshot: CodexCliSyncRollbackSnapshot | null,
377+
): snapshot is CodexCliSyncRollbackSnapshot {
378+
return Boolean(snapshot?.name.trim() && snapshot.path.trim());
379+
}
380+
375381
async function findLatestManualRollbackRun(): Promise<
376382
CodexCliSyncRun | null
377383
> {
378384
const history = await readSyncHistory({ kind: "codex-cli-sync" });
385+
let fallbackRun: CodexCliSyncRun | null = null;
379386
for (let index = history.length - 1; index >= 0; index -= 1) {
380387
const entry = history[index];
381388
if (!entry || entry.kind !== "codex-cli-sync") continue;
382389
const run = normalizeCodexCliSyncRun(entry.run);
383-
if (isManualChangedSyncRun(run)) {
390+
if (!isManualChangedSyncRun(run)) {
391+
continue;
392+
}
393+
fallbackRun ??= run;
394+
if (hasUsableRollbackSnapshot(run.rollbackSnapshot)) {
384395
return run;
385396
}
386397
}
387-
return null;
398+
return fallbackRun;
388399
}
389400

390401
async function loadRollbackSnapshot(

test/codex-cli-sync.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,98 @@ describe("codex-cli sync", () => {
15751575
expect(restored?.accounts[0]?.refreshToken).toBe("refresh-old");
15761576
});
15771577

1578+
it.each([
1579+
["null checkpoint", null],
1580+
[
1581+
"blank checkpoint path",
1582+
{
1583+
name: "accounts-codex-cli-sync-snapshot-invalid",
1584+
path: " ",
1585+
},
1586+
],
1587+
] satisfies Array<
1588+
[
1589+
string,
1590+
{ name: string; path: string } | null,
1591+
]
1592+
>)(
1593+
"falls back to the newest valid rollback checkpoint when a newer manual change has a %s",
1594+
async (_label, invalidSnapshot) => {
1595+
const snapshotPath = join(tempDir, "rollback-fallback-snapshot.json");
1596+
await writeFile(
1597+
snapshotPath,
1598+
JSON.stringify(
1599+
{
1600+
version: 3,
1601+
accounts: [
1602+
{
1603+
accountId: "acc_old",
1604+
accountIdSource: "token",
1605+
email: "old@example.com",
1606+
refreshToken: "refresh-old",
1607+
accessToken: "access-old",
1608+
addedAt: 1,
1609+
lastUsed: 1,
1610+
},
1611+
],
1612+
activeIndex: 0,
1613+
activeIndexByFamily: { codex: 0 },
1614+
} satisfies AccountStorageV3,
1615+
null,
1616+
2,
1617+
),
1618+
"utf-8",
1619+
);
1620+
1621+
const summary = {
1622+
sourceAccountCount: 1,
1623+
targetAccountCountBefore: 1,
1624+
targetAccountCountAfter: 1,
1625+
addedAccountCount: 0,
1626+
updatedAccountCount: 1,
1627+
unchangedAccountCount: 0,
1628+
destinationOnlyPreservedCount: 0,
1629+
selectionChanged: false,
1630+
};
1631+
const validRun: CodexCliSyncRun = {
1632+
outcome: "changed",
1633+
runAt: 10,
1634+
sourcePath: accountsPath,
1635+
targetPath: targetStoragePath,
1636+
summary,
1637+
trigger: "manual",
1638+
rollbackSnapshot: {
1639+
name: "accounts-codex-cli-sync-snapshot-valid",
1640+
path: snapshotPath,
1641+
},
1642+
};
1643+
const newerInvalidRun: CodexCliSyncRun = {
1644+
outcome: "changed",
1645+
runAt: 20,
1646+
sourcePath: accountsPath,
1647+
targetPath: targetStoragePath,
1648+
summary,
1649+
trigger: "manual",
1650+
rollbackSnapshot: invalidSnapshot,
1651+
};
1652+
1653+
await appendSyncHistoryEntry({
1654+
kind: "codex-cli-sync",
1655+
recordedAt: validRun.runAt,
1656+
run: validRun,
1657+
});
1658+
await appendSyncHistoryEntry({
1659+
kind: "codex-cli-sync",
1660+
recordedAt: newerInvalidRun.runAt,
1661+
run: newerInvalidRun,
1662+
});
1663+
1664+
const plan = await getLatestCodexCliSyncRollbackPlan();
1665+
expect(plan.status).toBe("ready");
1666+
expect(plan.snapshot).toEqual(validRun.rollbackSnapshot);
1667+
},
1668+
);
1669+
15781670
it("marks the rollback plan unavailable when the checkpoint file is missing", async () => {
15791671
const missingRun: CodexCliSyncRun = {
15801672
outcome: "changed",

0 commit comments

Comments
 (0)