docs: tomorrow-morning test basis (AI, S/MIME web+mobile, Theme); fix JMAP_SERVER_URL landmine in dev env example

Concrete, runnable test steps per area with explicit known-gaps sections
so nothing reads as more finished than it is. Also documents the mobile
S/MIME merge (vncmail-native main 7b89839) done this session.

.env.dev.example: the documented relative JMAP_SERVER_URL 400s
/api/auth/stalwart-context (resolveTrustedJmapUrl rejects relative URLs),
silently breaking the real session-cookie flow that S/MIME enrollment,
offline sync, and the AI server/retrieval routes all depend on. Switched
the example to an absolute URL with an explanatory comment.
This commit is contained in:
Bernd Rodler
2026-08-06 00:51:37 +02:00
parent daa40ec72d
commit 61651b1ed1
2 changed files with 241 additions and 3 deletions
+9 -3
View File
@@ -15,9 +15,15 @@
DEV_MOCK_JMAP=true
# Point the app at its own mock endpoint.
# IMPORTANT: This must match the origin the app runs on (default: port 3000).
# Using a different port (e.g. 3001) will cause CORS errors.
JMAP_SERVER_URL=/api/dev-jmap
# IMPORTANT: must be an ABSOLUTE URL matching the origin the app runs on
# (default: port 3000) - NOT a relative path. A relative path here makes
# /api/auth/stalwart-context 400 on every request (resolveTrustedJmapUrl
# rejects it), which silently breaks the real server-side session-cookie
# flow that S/MIME enrollment, offline sync, and the AI server/retrieval
# routes all depend on. The client-side mock fetch works either way, which
# is why this is easy to miss - it only bites features needing a real
# server-side session identity.
JMAP_SERVER_URL=http://localhost:3000/api/dev-jmap
# =============================================================================
# App
+232
View File
@@ -0,0 +1,232 @@
# Test basis — 2026-08-06 morning
Covers everything shipped overnight: AI Assistant (web), S/MIME (web + mobile),
and the 2-theme rebrand. For each area: how to get into a testable state,
concrete steps with expected results, and — importantly — what's still a
known gap so nothing here gets mistaken for more finished than it is.
All web steps assume `vncmail-plus` running locally via the built-in mock
JMAP server (fastest path, no real Stalwart needed):
```bash
cp .env.dev.example .env.local
# then set, in .env.local:
# AI_SERVER_BASE_URL=http://127.0.0.1:11434 (real Ollama on this Mac)
# SMIME_CA_DEV_LOCAL=true
npm run dev
```
Open `http://localhost:3000` and **log in via the "Anmelden" (dev-mode
login) button, not "Demo starten"**. Demo mode is a pure client-side
in-memory session with no server-side cookie — AI's `server` class, S/MIME
enrollment, and offline routes all need the real cookie the login button
sets, and will look broken (401s) under Demo mode for reasons that have
nothing to do with the features themselves.
---
## 1. AI Assistant (web)
Settings → AI Assistant.
### 1.1 Local (Ollama on this Mac)
1. Select provider **Local**. Model list should populate from the running
`ollama serve` (confirm with `ollama list` in a terminal first).
2. Ask a question in "Try it" with no special mail content, e.g. "reply with
exactly the words LOCAL AI WORKS".
3. **Expect:** exact echo back, no seat/entitlement banner (local is free,
unmetered, never reaches this app's own backend).
### 1.2 Server (this app's backend → your Ollama)
1. Select provider **Server**. Model list comes from `/api/ai/server/models`
(same models as Local, proxied — confirms the same-origin proxy path
works, not just direct-to-loopback).
2. Ask a first question. **Expect:** a small "seat assigned" notice on the
*first* successful call for this user, none on subsequent calls.
3. Check `data/admin-state/ai-metering.jsonl` on disk — a new line should
appear with real `promptTokens`/`completionTokens`/`latencyMs` from the
actual Ollama call, not placeholders.
4. To test the entitlement *ceiling*: `curl -X PUT localhost:3000/api/admin/ai/entitlement -H 'Content-Type: application/json' -d '{"seatsTotal":0}'`
(needs an admin session cookie), then retry a Server-class question as a
*different* username. **Expect:** HTTP 402, "no licensed seats configured".
Set `seatsTotal` back up afterwards.
### 1.3 Public (BYOK)
1. Add a profile: name, model, base URL, API key (any real provider you
have a key for, or a fake key just to test the UI — the request itself
will fail at the provider, not in this app).
2. Add a *second* profile with a different name/model.
3. In "Try it", use the **"Answer with"** selector to choose between the two
profiles for the same question. **Expect:** each answers using its own
configured model, no cross-talk.
4. Remove a profile from the list. **Expect:** it disappears immediately,
and its key is gone from `localStorage` (`DevTools → Application →
Local Storage`, key prefix `vncmail.ai.key.`).
### 1.4 Retrieval (grounded answers over your own mail)
The demo/mock inbox includes a "Villa sul Lago" booking confirmation email.
1. On **Server** or **Local**, ask: *"When is check-in for the Villa sul
Lago booking?"*
2. **Expect:** a specific date/time, with a cited source referencing the
actual email (not a generic non-answer). This exercises the full
pipeline: real JMAP `Email/query`+`Email/get`, real Ollama embeddings
(`nomic-embed-text`), cosine similarity, RRF fusion with the local
text-match leg.
### 1.5 Known gaps — don't be surprised by these
- **No admin UI yet for any of the above.** Seat totals, model allow-lists,
per-class on/off, usage — all real on the backend (§1.2, §1.3) but only
reachable via `curl`/direct API calls today. A full spec + visual mockup
for the admin console exists at
[`docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md`](ADMIN-AI-POLICY-CONSOLE-SPEC.md)
— presented for approval, **intentionally not built** pending sign-off.
- **BYOK provider allow-listing does not exist at all yet** — a user can
point a Public profile at literally any URL. Not a regression, just never
built; see the spec's §6.1 for the proposed (client-side-only) approach.
- Tier (base/standard/pro) is stored but doesn't change behavior yet — only
`seatsTotal` gates anything.
---
## 2. S/MIME — web
### 2.1 What to test today
With `SMIME_CA_DEV_LOCAL=true` set, the CA-issuance backend is real and
independently verified (4 passing tests: CSR signature verification, full
chain verification via `leaf.verify(caCert)`, correct SAN/rfc822Name
addresses, `emailProtection` EKU present — see
`lib/smime-ca/__tests__/local-dev-provider.test.ts`).
There is **no user-facing "Enroll" button wired up yet** — that was a
deliberate scope decision made overnight (see the commit message on
`c2c07293`), not an oversight: this plugin has known, still-open
security-audit findings, and generating brand-new CSR/key-import code
against it at 1am risked introducing a new one rather than closing an old
one. So today, test the backend directly:
```bash
# Generate a throwaway CSR (any tool - openssl shown here):
openssl req -new -newkey rsa:2048 -nodes -keyout /tmp/test.key \
-out /tmp/test.csr -subj "/CN=Test User"
# Call the enrollment route directly (needs an authenticated session cookie
# from the "Anmelden" login above - copy it from DevTools → Application →
# Cookies, or use a logged-in curl session):
curl -X POST localhost:3000/api/smime/enroll \
-H 'Content-Type: application/json' \
--cookie "<your session cookie>" \
-d "{\"csrPem\": $(python3 -c 'import json,sys; print(json.dumps(open("/tmp/test.csr").read()))'), \"addresses\": [\"you@example.com\"]}"
```
**Expect:** a JSON response with `certificatePem`, `chainPem`, `serialNumber`,
`notAfter`. Verify it's a real cert: `openssl x509 -in <(echo "$certificatePem") -noout -text`
should show issuer `VNCmail+ LOCAL DEV S/MIME CA`, your `you@example.com` in
Subject Alternative Name, and Extended Key Usage including `E-mail
Protection`.
### 2.2 Known gap — the important one
**Nobody can actually enroll from the UI today.** The plugin's Settings
section still has no "Get a certificate" button, no client-side CSR
generation, and no wiring to import an issued cert into its existing
encrypted-at-rest key storage (`vnc/plugins/smime/src/pkcs12.js`'s
AES-GCM+PBKDF2 convention). The plugin itself is confirmed *mounted*
(privileged iframe, same-origin fetch works without a bridge) with 0 certs
imported — that's expected, not a bug. This is real, well-scoped follow-up
work, not blocked on anything — just deliberately not rushed overnight.
---
## 3. S/MIME — mobile (vncmail-native)
Unlike web, mobile got the **full stack**, merged to `main` this session
(`7b89839`, combining the S/MIME work with the AI/offline-sync work that
landed on `main` in parallel): real CMS sign/verify/encrypt/decrypt on
`node-forge` primitives (wire-compatible with the audited webmail plugin,
verified byte-for-byte against OpenSSL), PKCS#12 import, keys wrapped
AES-256-GCM/PBKDF2 in `expo-secure-store` (Keychain/Keystore — never
AsyncStorage in the clear).
### 3.1 Test steps
1. Build and run on a simulator/device (`npm run ios` / `npm run android`
from `vncmail-native`).
2. Settings → S/MIME → **Import certificate**. Use a real `.p12` file (an
OpenSSL-generated one works fine for testing) and its passphrase.
3. **Expect:** cert imported, persists across an app restart, a wrong
passphrase is rejected, the right one unlocks it.
4. Compose a new email to yourself → enable **Sign** (and/or **Encrypt**) →
send.
5. Open the received message. **Expect:** a signature banner showing
verified/signed-by, and if encrypted, the body decrypts and displays
normally.
6. Try receiving a deliberately malformed/unauthenticated-cipher message
(or just trust the 259-line hardening test suite —
`src/lib/__tests__/smime-hardening.test.ts` — covers this without
needing to hand-craft one). **Expect:** unauthenticated content renders
as inert plaintext, never live HTML.
### 3.2 Known-good, already verified
479+ unit tests (834 after the merge with the AI/sync work) pass, including
a full round-trip test. `typecheck` is clean. Also verified live on an
Android emulator during that session: real OpenSSL-3-generated `.p12`
imported through the actual device file picker.
### 3.3 Known gap
No cross-checking was done *this session* between the mobile
implementation and the CA now issuing certs on web (§2) — they were built
independently and haven't been tested importing a *web-CA-issued*
certificate into the mobile app. Worth doing once web's Enroll UI (§2.2)
exists, not before.
---
## 4. Theme
Settings → Themes. Exactly 2 built-in themes now: **SRC** (default, Swiss
red `#D52B1E`) and **VNClagoon** (navy/cyan). The other 6 generic built-ins
(Nord, Catppuccin, Solarized, Roundcube Elastic, Aurora Glass, plus the old
default) are hidden via admin theme policy, not deleted — an admin could
re-enable them, a normal user can't see them.
### 4.1 Test steps
1. Confirm SRC is active on first load (no theme ever selected before).
2. Switch to VNClagoon. **Expect:** a "Theme activated" toast, and the
chrome genuinely re-skins (navy/cyan replaces red throughout — sidebar,
buttons, unread markers, not just an accent color here and there).
3. Switch back to SRC. **Expect:** same, in reverse.
4. Search the UI (empty-state illustrations, manifest/PWA name, login page,
locale strings in a couple of languages) for the string "Bulwark" — all
24 locale files were rebranded to "VNCmail+" this session, plus
`app/manifest.ts`, demo fixtures, and the inbox empty-state logo, which
now resolves through the same theme-logo system every other themed
surface uses instead of a hardcoded path.
### 4.2 Known gap
Not exhaustively re-checked for every locale/every screen — the 24-file
find-and-replace was mechanical (script-driven) and spot-checked, not
manually walked screen-by-screen in each language. If a stray "Bulwark"
turns up somewhere, it's most likely a screen that wasn't in scope of the
locale-string sweep (e.g. a hardcoded string in a component rather than a
translation key) — same category as the logo fix above, worth a quick grep
(`grep -ril bulwark`) if one surfaces.
---
## 5. Offline / mail-index
Already QA'd and fixed this session (not re-listed as a to-do): a leaked
SQLite handle on `PRAGMA` failure, and contacts/files that weren't being
removed from the local index on delete. Both fixed and covered by tests —
no separate manual test needed tomorrow unless something else turns up.