Skip to content

Commit 736d4eb

Browse files
committed
fix(storage): rethrow reappeared export locks
1 parent 97a8b18 commit 736d4eb

2 files changed

Lines changed: 115 additions & 7 deletions

File tree

lib/storage.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -755,24 +755,45 @@ async function migrateLegacyProjectStorageIfNeeded(options?: {
755755
log.warn(message, details);
756756
},
757757
});
758-
const readOnlyExportCurrentStorage = async (): Promise<{
758+
const readLiveCurrentStorageIfExportMode = async (): Promise<{
759759
exists: boolean;
760760
storage: AccountStorageV3 | null;
761761
}> => {
762762
if (commit || !existsSync(currentStoragePath)) {
763763
return { exists: false, storage: null };
764764
}
765-
return {
766-
exists: true,
767-
storage: await loadCurrentStorageForMigration(),
768-
};
765+
try {
766+
const { normalized, schemaErrors } = await loadAccountsFromPath(
767+
currentStoragePath,
768+
{
769+
normalizeAccountStorage,
770+
isRecord,
771+
},
772+
);
773+
if (schemaErrors.length > 0) {
774+
log.warn("current account storage schema validation warnings", {
775+
path: currentStoragePath,
776+
errors: schemaErrors.slice(0, 5),
777+
});
778+
}
779+
return {
780+
exists: true,
781+
storage: normalized,
782+
};
783+
} catch (error) {
784+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
785+
return { exists: false, storage: null };
786+
}
787+
throw error;
788+
}
769789
};
770790

771791
let targetStorage = await loadCurrentStorageForMigration();
772792
let migrated = false;
773793

774794
for (const legacyPath of existingCandidatePaths) {
775-
const liveCurrentStorageBeforeMerge = await readOnlyExportCurrentStorage();
795+
const liveCurrentStorageBeforeMerge =
796+
await readLiveCurrentStorageIfExportMode();
776797
if (liveCurrentStorageBeforeMerge.exists) {
777798
return liveCurrentStorageBeforeMerge.storage;
778799
}
@@ -796,7 +817,7 @@ async function migrateLegacyProjectStorageIfNeeded(options?: {
796817
}
797818

798819
const liveCurrentStorageAfterLegacyRead =
799-
await readOnlyExportCurrentStorage();
820+
await readLiveCurrentStorageIfExportMode();
800821
if (liveCurrentStorageAfterLegacyRead.exists) {
801822
return liveCurrentStorageAfterLegacyRead.storage;
802823
}

test/storage.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,6 +1752,93 @@ describe("storage", () => {
17521752
}
17531753
});
17541754

1755+
it.each(["EBUSY", "EPERM", "EAGAIN"] as const)(
1756+
"rethrows %s when the current storage reappears locked during export fallback",
1757+
async (code) => {
1758+
const currentStoragePath = join(
1759+
testWorkDir,
1760+
`accounts-reappeared-locked-${code}.json`,
1761+
);
1762+
const legacyStoragePath = join(
1763+
testWorkDir,
1764+
`accounts-reappeared-legacy-${code}.json`,
1765+
);
1766+
await fs.writeFile(
1767+
legacyStoragePath,
1768+
JSON.stringify({
1769+
version: 3,
1770+
activeIndex: 0,
1771+
activeIndexByFamily: {},
1772+
accounts: [
1773+
{
1774+
accountId: "legacy",
1775+
refreshToken: "legacy-token",
1776+
addedAt: 1,
1777+
lastUsed: 1,
1778+
},
1779+
],
1780+
}),
1781+
);
1782+
1783+
const actualStorageParser = await vi.importActual<
1784+
typeof import("../lib/storage/storage-parser.js")
1785+
>("../lib/storage/storage-parser.js");
1786+
let currentReadCount = 0;
1787+
vi.resetModules();
1788+
vi.doMock("../lib/storage/storage-parser.js", () => ({
1789+
...actualStorageParser,
1790+
loadAccountsFromPath: vi.fn(async (path, deps) => {
1791+
if (path === currentStoragePath) {
1792+
currentReadCount += 1;
1793+
if (currentReadCount === 1) {
1794+
await fs.writeFile(
1795+
currentStoragePath,
1796+
JSON.stringify({
1797+
version: 3,
1798+
activeIndex: 0,
1799+
activeIndexByFamily: {},
1800+
accounts: [],
1801+
}),
1802+
);
1803+
throw Object.assign(
1804+
new Error("missing current storage"),
1805+
{ code: "ENOENT" },
1806+
);
1807+
}
1808+
throw Object.assign(new Error(`locked ${code}`), { code });
1809+
}
1810+
return actualStorageParser.loadAccountsFromPath(path, deps);
1811+
}),
1812+
}));
1813+
1814+
try {
1815+
const isolatedStorageModule = await import("../lib/storage.js");
1816+
const isolatedPathState = await import("../lib/storage/path-state.js");
1817+
isolatedPathState.setStoragePathState({
1818+
currentStoragePath,
1819+
currentLegacyProjectStoragePath: legacyStoragePath,
1820+
currentLegacyWorktreeStoragePath: null,
1821+
currentProjectRoot: null,
1822+
});
1823+
1824+
await expect(
1825+
isolatedStorageModule.exportAccounts(exportPath),
1826+
).rejects.toMatchObject({ code });
1827+
1828+
const currentStorage = JSON.parse(
1829+
await fs.readFile(currentStoragePath, "utf-8"),
1830+
);
1831+
expect(currentStorage.accounts).toEqual([]);
1832+
expect(existsSync(legacyStoragePath)).toBe(true);
1833+
expect(existsSync(exportPath)).toBe(false);
1834+
} finally {
1835+
vi.doUnmock("../lib/storage/storage-parser.js");
1836+
vi.resetModules();
1837+
setStoragePathDirect(testStoragePath);
1838+
}
1839+
},
1840+
);
1841+
17551842
it("does not revive legacy accounts when the current storage has an intentional reset marker", async () => {
17561843
const currentStoragePath = join(testWorkDir, "accounts-reset-current.json");
17571844
const legacyStoragePath = join(testWorkDir, "accounts-reset-legacy.json");

0 commit comments

Comments
 (0)