Commit Graph
4 Commits
Author SHA1 Message Date
Bernd RodlerandClaude Sonnet 5 0271df4338 fix(mail-index): real end-to-end verification, and the three bugs it found
Adds integration/tests/12-electron-mail-index.spec.ts (3 tests, all passing
against the real Stalwart fixture) and fixes what running it exposed. None of
these were visible from reading the code.

1. JMAP session fetch never followed a redirect. Stalwart 307-redirects
   /.well-known/jmap to /jmap/session, and fetchJmapSession used
   `redirect: 'manual'` and treated any non-2xx as failure - so every reindex
   died with "JMAP session fetch failed (307)". Now follows up to 3 hops and
   REFUSES to follow off-origin, because the user's credentials ride on every
   hop; a blind `redirect: 'follow'` would hand the Authorization header to
   whatever host a misconfigured session pointed at. Same bound and same
   reasoning as lib/auth/verify-jmap-auth.ts.

2. The fd-3 key channel could only be adopted once per process, but its state
   was module-scoped. Next re-evaluates route modules, so a second instance hit
   `Could not open fd 3: Error: open EEXIST` from libuv. State moved to a
   Symbol on globalThis - the one place in a Node process that survives module
   re-evaluation.

3. Next's output file tracing does NOT carry @signalapp/sqlcipher's prebuilds/
   into .next/standalone. It traced the package's JS and its node-gyp-build
   dependency, but node-gyp-build resolves the .node binary by scanning a
   directory at runtime, which no static tracer can follow - so `require()`
   would have failed in every packaged build. scripts/assemble-standalone.mjs
   now copies it, alongside the public/ and .next/static copies it already does
   for the same "standalone output omits things" reason. All six platform/arch
   prebuilds are copied, not just this host's, because electron-builder
   cross-builds the x64 and arm64 macOS targets from one runner.

The three tests, and why it takes three - two constraints made a single
configuration impossible, and both were measured rather than assumed:

  * The renderer cannot reach this fixture from a production build. Its CSP
    pins connect-src to `'self' https: wss:` and the fixture's Stalwart is
    plain HTTP. NODE_ENV=development at RUNTIME does not help: `next build`
    INLINES process.env.NODE_ENV into the compiled middleware, so proxy.ts's
    `isDev` is frozen at build time (observed: a standalone server started with
    NODE_ENV=development still served the production CSP).
  * The fd-3 channel cannot survive `next dev`, which forks its server with an
    IPC channel that claims fd 3 (EEXIST); fd 4 there is not a pipe either
    (ENOTTY).

  So: PIPELINE drives the real standalone server over HTTP from Node with a
  real fd-3 key channel (no browser, so no CSP) and asserts a real SMTP
  delivery is findable by a word from its BODY, with a real snippet and
  contextBlock, idempotent catch-up, working type filters, and - reading the
  raw bytes of the .db AND its -wal - that nothing is recoverable in cleartext.
  TRIGGER proves the event-driven wiring: a real delivery makes the renderer
  POST /api/offline/reindex off its live push. WIRING launches the real shell
  with no ELECTRON_LOAD_URL and asserts the routes are reachable (401, not 404
  or 503) with real safeStorage behind them.

Each test now gets its own --user-data-dir. That is load-bearing, not hygiene:
Electron reuses one profile across launches, and a leftover jmap_stalwart_ctx
cookie from an earlier run made the WIRING test's 401 assertion pass as a 200.

Verified: typecheck clean; unit suite 2379 tests with the SAME 3 pre-existing
failures as the base commit b15098a6 (2 builtin-themes, 1 jmap-client-
resilience) and 48 net new passing; both `docker build`s succeed; the
hosted-deployment gate returns 404 with an empty body and materialises no file
in the production image; e2e/electron-smoke 4/4; 11-electron-notification
still passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 23:43:52 +02:00
Bernd Rodler 0f15132ec0 test(electron): real end-to-end push -> native notification, via SMTP
Phase 1 step 7 of VNCprodbuild. integration/tests/11-electron-notification.spec.ts
launches the actual Electron shell, logs in as alice against this repo's
existing docker-compose Stalwart fixture, injects a message over real SMTP
(same helpers/smtp.ts sendMail() 02-mail-sync.spec.ts uses), and asserts a
native notification fires via electron/main.ts's __notificationCallCount
test hook - proving the full real pipeline, not just the synthetic IPC call
step 3's smoke test exercises: SMTP -> Stalwart -> JMAP push
(lib/jmap/client.ts) -> stores/email-store.ts's handleStateChange ->
handleNewEmailNotification -> the page effect -> lib/electron-bridge.ts ->
the contextBridge/IPC bridge -> electron/main.ts's Notification call.

Runs against a `next dev` server (electron/main.ts's new ELECTRON_LOAD_URL
escape hatch), not the standalone build, because this fixture's Stalwart is
deliberately plain HTTP and production's CSP correctly refuses non-TLS
connections - the identical trade-off integration/webmail.Dockerfile already
makes for the browser-based suite. New playwright.integration-electron.config.ts
+ global-setup-electron.ts (brings up only the `stalwart` compose service,
not `webmail`, which this suite never touches and which may not even be
startable on a given host - see its own header comment) keep this fully
separate from the main dockerized integration run, which has no Electron
binary compatible with that container's platform; playwright.integration.config.ts
gets a matching testIgnore so a plain `npm run test:integration` never tries
to sweep this file in. Wired as `npm run test:integration:electron`.

On "the real WebSocket path": confirmed against this fixture's actual
`stalwartlabs/stalwart:v0.16` (same as the sandbox server) that its
/jmap/ws requires the same Authorization header as every other JMAP
endpoint on the handshake itself, which the browser WebSocket API cannot
attach - so the WS attempt reaches the network correctly (see the CSP fix
in the previous commit) but always fails auth here, and the circuit
breaker falls back to SSE within about a second. That fallback is what
delivers the push this test observes - documented in detail in the spec's
header comment, including why asserting the WS handshake itself succeeds
here would be asserting something that cannot be true from a browser
against this specific server.

Known flakiness, root-caused not eliminated (see
playwright.integration-electron.config.ts's retries: 2 and its comment):
`next dev`'s on-demand route compilation + Fast Refresh occasionally races
the SSE stream during the login -> inbox transition and drops that one push
event with no error anywhere - reproduced by running the identical test
repeatedly against an already-warm stack (IT_NO_DOCKER=1): identical
request sequence logged every time, but the outcome wasn't always the same.
This is specific to the dev-server workaround this test needs for the
plaintext-Stalwart fixture, not a bug in the feature it's verifying - the
WS circuit breaker and SSE fallback fire exactly as designed in every run's
own logs, pass or fail.

Verified: passed cleanly standalone multiple times; with retries: 2 in
place, passed within the retry budget on every attempt made.
2026-08-04 14:18:40 +02:00
Stefan HildebrandtandLinus Rath 60b9ae66ea test(integration): add IT_VIDEO option to record test videos
Make Playwright's video capture configurable via IT_VIDEO (on | off |
retain-on-failure [default] | on-first-retry) so a whole run — passing tests
included — can be recorded, e.g. for a demo or to inspect a flow. Forward the
env through the Playwright container in run-tests.sh and document it in the
README's environment-knobs table.
2026-07-21 20:56:27 +02:00
Stefan HildebrandtandLinus Rath 8d8bc7cb13 test(integration): dockerized webmail⇆Stalwart Playwright sync suite
Add an end-to-end integration harness that runs the webmail against a real
Stalwart mail server in Docker and drives it with Playwright, focused on the
mail/folder synchronisation behaviour (unread/total counters, folder-list
sync, account-scoped Unified Mailbox) that the unified-mailbox work touches.

- integration/ stack: Stalwart (declarative bootstrap: alice/bob/carol,
  submission + IMAP listeners, permissive CORS) + webmail (dev mode, so the
  browser's plaintext cross-origin JMAP calls aren't blocked by the prod CSP).
- Helpers: dependency-free SMTP submit client, JMAP client for seeding /
  inspecting server state, and page helpers (login, add/switch account,
  locale-independent folder-counter reads).
- Specs: login, single-account sync (receive/read/move/delete/folder-create/
  burst) and multi-account (per-account isolation + cross-account Unified
  Inbox aggregation + background-account delivery). 12 tests, all green.
- Add focused data-testid hooks to the mail UI (folder rows + counters, email
  list items, account switcher, composer) for stable selectors.
- Exclude examples/ and integration/ from tsconfig/eslint/.dockerignore.
2026-07-16 17:59:52 +02:00