diff --git a/src/device-sync/__tests__/app/lifecycle.test.ts b/src/device-sync/__tests__/app/lifecycle.test.ts index b95b0bb1..18729359 100644 --- a/src/device-sync/__tests__/app/lifecycle.test.ts +++ b/src/device-sync/__tests__/app/lifecycle.test.ts @@ -43,7 +43,8 @@ vi.mock('../../native', () => ({ ensureAndroidAccount: vi.fn(async (name: string, registryId: string) => { h.state.log.push(`ensure ${name}`); const existing = h.find(name); - if (existing && existing.registryId !== registryId) throw new Error('conflict'); + // As the native module rejects (DeviceSyncAccounts.ensure → AccountConflictException). + if (existing && existing.registryId !== registryId) throw Object.assign(new Error(`${name} belongs to another app account`), { code: 'conflict' }); if (existing) return false; h.state.accounts.push({ name, registryId, sync: {} }); return true; @@ -212,6 +213,18 @@ describe('enable', () => { expect(outcome).toEqual({ kind: 'enabled', accountName: ALICE }); }); + it('adds no second account named after the registry id when adding the account fails for another reason', async () => { + for (const code of ['failed', 'permission']) { + vi.mocked(native.ensureAndroidAccount).mockClear(); + vi.mocked(native.ensureAndroidAccount).mockRejectedValueOnce(Object.assign(new Error(`AccountManager said no (${code})`), { code })); + const outcome = await enableDeviceSync(ALICE, CONTACTS_AUTHORITY); + expect(outcome).toEqual({ kind: 'failed', message: `AccountManager said no (${code})` }); + expect(native.ensureAndroidAccount).toHaveBeenCalledTimes(1); + expect(h.state.accounts).toEqual([]); + expect(accountDeviceSync(ALICE).androidAccountName).toBeUndefined(); + } + }); + it('leaves no account behind when turning sync on fails', async () => { h.state.failSetSyncEnabled = true; const outcome = await enableDeviceSync(ALICE, CONTACTS_AUTHORITY); diff --git a/src/device-sync/app/lifecycle.ts b/src/device-sync/app/lifecycle.ts index 4daba392..6f058c71 100644 --- a/src/device-sync/app/lifecycle.ts +++ b/src/device-sync/app/lifecycle.ts @@ -122,8 +122,9 @@ async function ensureAccountFor(registryId: string): Promise { await ensureAndroidAccount(name, registryId); return name; } catch (err) { - // The name belongs to another app account after all (the list was stale). - if (name === registryId) throw err; + // The name belongs to another app account after all (the list was stale). Any + // other failure fails the turn-on: a second account would show in the apps. + if (name === registryId || (err as { code?: unknown } | null)?.code !== 'conflict') throw err; await ensureAndroidAccount(registryId, registryId); return registryId; }