Skip to content

fix: avoid ghost WhatsApp channels after QR cancel#44

Merged
lsave merged 4 commits intomainfrom
codex/fix-whatsapp-ghost-channel
Mar 27, 2026
Merged

fix: avoid ghost WhatsApp channels after QR cancel#44
lsave merged 4 commits intomainfrom
codex/fix-whatsapp-ghost-channel

Conversation

@lsave
Copy link
Copy Markdown
Contributor

@lsave lsave commented Mar 27, 2026

Summary:

  • detect WhatsApp as configured only when a persisted creds.json exists, so empty login directories no longer surface a ghost built-in channel
  • clean up cancelled WhatsApp QR login directories only when they have not written persisted credentials, which preserves existing sessions for restarted logins
  • add focused regression coverage for empty dirs, persisted creds detection, and cancelled-login cleanup behavior

Validation:

  • pnpm exec vitest run tests/unit/whatsapp-credentials.test.ts
  • pnpm exec vitest run tests/unit/channel-config.test.ts
  • pnpm exec tsc --noEmit --pretty false

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request centralizes WhatsApp credential management by introducing a new utility file and refactoring existing logic to use it. It also adds a cleanup mechanism for cancelled login attempts in the WhatsApp login manager. The review feedback correctly identifies that the new cleanupCancelledWhatsAppLogin function uses synchronous file system operations, which could block the Electron main thread and cause UI unresponsiveness. It is recommended to convert this function to be asynchronous and update the corresponding call sites and unit tests to handle the resulting promises.

Comment on lines +49 to +67
export function cleanupCancelledWhatsAppLogin(accountId: string): boolean {
const accountDir = getWhatsAppAccountCredentialsDir(accountId);
if (!existsSync(accountDir)) {
return false;
}

if (existsSync(getWhatsAppCredsFilePathForDir(accountDir))) {
return false;
}

rmSync(accountDir, { recursive: true, force: true });

const credentialsDir = getWhatsAppCredentialsDir();
if (existsSync(credentialsDir) && readdirSync(credentialsDir).length === 0) {
rmSync(credentialsDir, { recursive: true, force: true });
}

return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function cleanupCancelledWhatsAppLogin uses synchronous file system operations (existsSync, rmSync, readdirSync). Since this function is called from an async context, using synchronous I/O can block the main thread. It's better to use the asynchronous versions from fs/promises to avoid potential UI freezes or unresponsiveness. This also makes the file I/O style consistent within the file.

This change will require:

  1. Updating the imports in this file (removing sync fs methods and adding rm from fs/promises).
  2. Updating the call site in whatsapp-login.ts to await this function.
  3. Updating the corresponding test in tests/unit/whatsapp-credentials.test.ts.
export async function cleanupCancelledWhatsAppLogin(accountId: string): Promise<boolean> {
  const accountDir = getWhatsAppAccountCredentialsDir(accountId);
  if (!(await fileExists(accountDir))) {
    return false;
  }

  if (await fileExists(getWhatsAppCredsFilePathForDir(accountDir))) {
    return false;
  }

  await rm(accountDir, { recursive: true, force: true });

  const credentialsDir = getWhatsAppCredentialsDir();
  try {
    // Check if parent directory is now empty and remove it.
    if ((await readdir(credentialsDir)).length === 0) {
      await rm(credentialsDir, { recursive: true, force: true });
    }
  } catch {
    // Ignore errors, e.g. if credentialsDir doesn't exist anymore.
  }

  return true;
}

Comment on lines +423 to +425
if (cleanupAccountId && cleanupCancelledWhatsAppLogin(cleanupAccountId)) {
console.log(`[WhatsAppLogin] Cleaned up cancelled login state for ${cleanupAccountId}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To accompany the suggested change to make cleanupCancelledWhatsAppLogin asynchronous, this call should use await.

Suggested change
if (cleanupAccountId && cleanupCancelledWhatsAppLogin(cleanupAccountId)) {
console.log(`[WhatsAppLogin] Cleaned up cancelled login state for ${cleanupAccountId}`);
}
if (cleanupAccountId && (await cleanupCancelledWhatsAppLogin(cleanupAccountId))) {
console.log(`[WhatsAppLogin] Cleaned up cancelled login state for ${cleanupAccountId}`);
}

Comment on lines +116 to +117
expect(cleanupCancelledWhatsAppLogin('cancelled')).toBe(true);
expect(cleanupCancelledWhatsAppLogin('existing')).toBe(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If cleanupCancelledWhatsAppLogin is converted to an async function as suggested, these assertions will need to be updated to handle the returned Promise.

Suggested change
expect(cleanupCancelledWhatsAppLogin('cancelled')).toBe(true);
expect(cleanupCancelledWhatsAppLogin('existing')).toBe(false);
await expect(cleanupCancelledWhatsAppLogin('cancelled')).resolves.toBe(true);
await expect(cleanupCancelledWhatsAppLogin('existing')).resolves.toBe(false);

@lsave lsave merged commit fb23fda into main Mar 27, 2026
2 checks passed
@lsave lsave deleted the codex/fix-whatsapp-ghost-channel branch March 29, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant