Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.
This repository was archived by the owner on Jul 30, 2026. It is now read-only.

Compose tab state is never cleaned up when a compose window is closed #153

Description

@dobby-coder

What happens

cleanupComposeTab(tabId) is documented as "Drop all state associated with a compose tab (called on send / close)" (src/background/state.ts:135-138), but there is no close path:

  • src/background/sent-copy.ts:120 calls it on onAfterSend — the send half.
  • src/background/background.ts:12 imports cleanupComposeTab but never calls it — the import is dead, and no listener fires when a compose window is closed without sending (grep -rn "cleanupComposeTab\|tabs.onRemoved" src).

So every compose window the user opens and closes without sending — or whose send was cancelled by a guard (evaluateBeforeSendGuards BCC/policy-editor branches) or by a failed encryption — leaves its entry in the composeTabs map for the lifetime of the background script, including any policy / signId the user configured.

Why it matters

  • State leak: the map grows for the whole background session. Small per entry, but unbounded in a long-running Thunderbird.
  • Stale persisted state: persistEncryptState() writes every entry with encrypt: true to storage.local (state.ts:60-68). Entries for compose windows that no longer exist keep being rewritten. They are filtered on restore (restoreEncryptState checks the tab still exists) but only at background start, so the persisted blob accumulates dead tab ids in between.
  • Stale state can be inherited: windows.onCreated only sets a default when the tab id is absent (background.ts:142, deliberately — see CLAUDE.md). A leftover entry for a reused tab id means a fresh compose window silently starts with the previous window's encrypt flag and recipient policy instead of the shouldEncrypt default.

Suggested fix (small)

  • Register a compose-tab close listener in background.ts (next to the existing messages.onDeleted listener) that calls cleanupComposeTab(tabId), clears any in-flight upload record for that tab, and re-persists (persistEncryptState() / persistInFlightUploads()), mirroring what sent-copy.ts:118-123 does after a send.
  • browser.tabs.onRemoved is not in src/types/thunderbird.d.ts yet (only tabs.query is declared, line 22-24) — add the minimal declaration alongside the existing windows.onRemoved block at line 38.
  • The dead cleanupComposeTab import in background.ts disappears on its own once the listener uses it.
  • Cover it in tests/state.test.ts in the same style as the existing cleanupComposeTab test (line 62): entry present → close → entry gone and the persisted blob no longer lists the tab.

Separate second commit: CLAUDE.md is out of date

The "Tests not gated in CI" section of CLAUDE.md says .github/workflows/build.yml runs typecheck and build "but NOT npm test (tracked, unfixed)". That stopped being true in #151 — the workflow now has a Run tests step running npm test. The rest of that paragraph (tests are outside tsconfig.json's include, so test type errors are caught by neither typecheck nor vitest) is still accurate and should stay. Please fix the first sentence in its own commit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions