Skip to content

Commit f8590ca

Browse files
fix(auth): verify sqlite auth migration before cleanup
1 parent 56d201f commit f8590ca

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

src/commands/doctor-auth-flat-profiles.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,39 @@ describe("maybeMigrateAuthProfileJsonStoresToSqlite", () => {
409409
expect(fs.existsSync(authPath)).toBe(false);
410410
expect(fs.existsSync(`${authPath}.sqlite-import.458.bak`)).toBe(true);
411411
});
412+
413+
it("keeps legacy JSON when SQLite verification misses an imported profile", async () => {
414+
const state = await makeTestState();
415+
const authPath = await writeLegacyAuthProfilesJson(state, {
416+
version: 1,
417+
profiles: {
418+
"openrouter:default": {
419+
type: "api_key",
420+
provider: "openrouter",
421+
key: "sk-openrouter",
422+
},
423+
},
424+
});
425+
426+
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
427+
cfg: {},
428+
prompter: makePrompter(true),
429+
now: () => 464,
430+
deps: {
431+
loadPersistedAuthProfileStore: () => ({
432+
version: 1,
433+
profiles: {},
434+
}),
435+
},
436+
});
437+
438+
expect(result.changes).toStrictEqual([]);
439+
expect(result.warnings).toStrictEqual([
440+
`Left auth profile JSON in place for ${authPath} because SQLite verification did not find imported profile(s): openrouter:default.`,
441+
]);
442+
expect(fs.existsSync(authPath)).toBe(true);
443+
expect(fs.existsSync(`${authPath}.sqlite-import.464.bak`)).toBe(false);
444+
});
412445
});
413446

414447
describe("maybeRepairLegacyFlatAuthProfileStores", () => {

src/commands/doctor-auth-flat-profiles.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,46 @@ function mergeImportedAuthProfileState(params: {
354354
};
355355
}
356356

357+
function formatMissingAuthProfileSqliteVerification(params: {
358+
expected: AuthProfileStore;
359+
importedProfileIds: ReadonlySet<string>;
360+
loaded: AuthProfileStore | null;
361+
}): string | null {
362+
const missingProfileIds = [...params.importedProfileIds].filter(
363+
(profileId) => !params.loaded?.profiles[profileId],
364+
);
365+
const missingStateFields: string[] = [];
366+
for (const [provider, profileIds] of Object.entries(params.expected.order ?? {})) {
367+
const loadedProfileIds = params.loaded?.order?.[provider];
368+
if (
369+
!loadedProfileIds ||
370+
loadedProfileIds.length !== profileIds.length ||
371+
loadedProfileIds.some((profileId, index) => profileId !== profileIds[index])
372+
) {
373+
missingStateFields.push(`order.${provider}`);
374+
}
375+
}
376+
for (const [provider, profileId] of Object.entries(params.expected.lastGood ?? {})) {
377+
if (params.loaded?.lastGood?.[provider] !== profileId) {
378+
missingStateFields.push(`lastGood.${provider}`);
379+
}
380+
}
381+
for (const profileId of Object.keys(params.expected.usageStats ?? {})) {
382+
if (!params.loaded?.usageStats?.[profileId]) {
383+
missingStateFields.push(`usageStats.${profileId}`);
384+
}
385+
}
386+
387+
const parts: string[] = [];
388+
if (missingProfileIds.length > 0) {
389+
parts.push(`imported profile(s): ${missingProfileIds.toSorted().join(", ")}`);
390+
}
391+
if (missingStateFields.length > 0) {
392+
parts.push(`auth state field(s): ${missingStateFields.toSorted().join(", ")}`);
393+
}
394+
return parts.length > 0 ? parts.join("; ") : null;
395+
}
396+
357397
function filterRawAuthProfileState(
358398
raw: Record<string, unknown>,
359399
shouldKeepProfileId: (profileId: string) => boolean,
@@ -483,9 +523,14 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
483523
prompter: Pick<DoctorPrompter, "confirmAutoFix">;
484524
now?: () => number;
485525
env?: NodeJS.ProcessEnv;
526+
deps?: {
527+
loadPersistedAuthProfileStore?: typeof loadPersistedAuthProfileStore;
528+
};
486529
}): Promise<LegacyFlatAuthProfileRepairResult> {
487530
const now = params.now ?? Date.now;
488531
const env = params.env ?? process.env;
532+
const loadMigratedStore =
533+
params.deps?.loadPersistedAuthProfileStore ?? loadPersistedAuthProfileStore;
489534
const candidates = listAuthProfileSqliteMigrationCandidates(params.cfg, env);
490535
const detected = candidates.filter(
491536
(candidate) =>
@@ -582,23 +627,30 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
582627
continue;
583628
}
584629

585-
const existing = loadPersistedAuthProfileStore(candidate.agentDir) ?? {
630+
const existing = loadMigratedStore(candidate.agentDir) ?? {
586631
version: AUTH_STORE_VERSION,
587632
profiles: {},
588633
};
589634
const existingProfileIds = new Set(Object.keys(existing.profiles));
590635
const existingState = coerceAuthProfileState(existing);
591636
let next: AuthProfileStore = { ...existing };
637+
const importedProfileIds = new Set<string>();
592638
if (legacyStore) {
593639
const legacyAsStore: AuthProfileStore = { version: AUTH_STORE_VERSION, profiles: {} };
594640
applyLegacyAuthStore(legacyAsStore, legacyStore);
641+
for (const profileId of Object.keys(legacyAsStore.profiles)) {
642+
importedProfileIds.add(profileId);
643+
}
595644
next = mergeImportedAuthProfiles({
596645
store: next,
597646
profiles: legacyAsStore.profiles,
598647
existingProfileIds,
599648
});
600649
}
601650
if (canonicalStore) {
651+
for (const profileId of Object.keys(canonicalStore.profiles)) {
652+
importedProfileIds.add(profileId);
653+
}
602654
next = {
603655
...next,
604656
version: Math.max(next.version, canonicalStore.version),
@@ -631,6 +683,17 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
631683
preserveStateProfileIds: stateProfileIds,
632684
syncExternalCli: false,
633685
});
686+
const verificationFailure = formatMissingAuthProfileSqliteVerification({
687+
expected: next,
688+
importedProfileIds,
689+
loaded: loadMigratedStore(candidate.agentDir),
690+
});
691+
if (verificationFailure) {
692+
result.warnings.push(
693+
`Left auth profile JSON in place for ${shortenHomePath(candidate.authPath)} because SQLite verification did not find ${verificationFailure}.`,
694+
);
695+
continue;
696+
}
634697
}
635698

636699
const backups: string[] = [];

0 commit comments

Comments
 (0)