Skip to content

Commit bf74f37

Browse files
ndycodecodex
andcommitted
fix(storage): suppress flagged reset revival
Co-authored-by: Codex <noreply@openai.com>
1 parent 833d4b6 commit bf74f37

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

lib/storage.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,7 @@ function normalizeFlaggedStorage(data: unknown): FlaggedAccountStorageV1 {
19711971

19721972
export async function loadFlaggedAccounts(): Promise<FlaggedAccountStorageV1> {
19731973
const path = getFlaggedAccountsPath();
1974+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
19741975
const empty: FlaggedAccountStorageV1 = { version: 1, accounts: [] };
19751976

19761977
try {
@@ -1983,6 +1984,10 @@ export async function loadFlaggedAccounts(): Promise<FlaggedAccountStorageV1> {
19831984
}
19841985
}
19851986

1987+
if (existsSync(resetMarkerPath) && !existsSync(path)) {
1988+
return empty;
1989+
}
1990+
19861991
const recovered = storageBackupEnabled ? await recoverFlaggedAccountsFromBackups(path) : null;
19871992
if (recovered) {
19881993
return recovered;
@@ -2042,6 +2047,7 @@ export async function saveFlaggedAccounts(storage: FlaggedAccountStorageV1): Pro
20422047
const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
20432048
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
20442049
await fs.rename(tempPath, path);
2050+
await removeIntentionalResetMarker(path);
20452051
} catch (error) {
20462052
try {
20472053
await fs.unlink(tempPath);
@@ -2057,6 +2063,18 @@ export async function saveFlaggedAccounts(storage: FlaggedAccountStorageV1): Pro
20572063
export async function clearFlaggedAccounts(): Promise<void> {
20582064
return withStorageLock(async () => {
20592065
const path = getFlaggedAccountsPath();
2066+
const markerPath = getIntentionalResetMarkerPath(path);
2067+
try {
2068+
await fs.mkdir(dirname(path), { recursive: true });
2069+
await writeIntentionalResetMarker(path);
2070+
} catch (error) {
2071+
log.error("Failed to write flagged reset marker", {
2072+
path,
2073+
markerPath,
2074+
error: String(error),
2075+
});
2076+
throw error;
2077+
}
20602078
const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
20612079
for (const candidate of [path, ...backupPaths]) {
20622080
try {

test/storage-flagged.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,52 @@ describe("flagged account storage", () => {
287287
expect(flagged.accounts).toHaveLength(0);
288288
});
289289

290+
it("suppresses flagged backup revival when clear only partially deletes backup artifacts", async () => {
291+
await saveFlaggedAccounts({
292+
version: 1,
293+
accounts: [
294+
{
295+
refreshToken: "partial-delete-primary",
296+
flaggedAt: 1,
297+
addedAt: 1,
298+
lastUsed: 1,
299+
},
300+
],
301+
});
302+
303+
await saveFlaggedAccounts({
304+
version: 1,
305+
accounts: [
306+
{
307+
refreshToken: "partial-delete-secondary",
308+
flaggedAt: 2,
309+
addedAt: 2,
310+
lastUsed: 2,
311+
},
312+
],
313+
});
314+
315+
const flaggedPath = getFlaggedAccountsPath();
316+
const backupPath = `${flaggedPath}.bak`;
317+
const originalUnlink = fs.unlink.bind(fs);
318+
const unlinkSpy = vi.spyOn(fs, "unlink").mockImplementation(async (targetPath) => {
319+
if (targetPath === backupPath) {
320+
const error = new Error("EACCES backup delete") as NodeJS.ErrnoException;
321+
error.code = "EACCES";
322+
throw error;
323+
}
324+
return originalUnlink(targetPath);
325+
});
326+
327+
await clearFlaggedAccounts();
328+
329+
const flagged = await loadFlaggedAccounts();
330+
expect(existsSync(backupPath)).toBe(true);
331+
expect(flagged.accounts).toHaveLength(0);
332+
333+
unlinkSpy.mockRestore();
334+
});
335+
290336
it("emits snapshot metadata for flagged account backups", async () => {
291337
await saveFlaggedAccounts({
292338
version: 1,

0 commit comments

Comments
 (0)