mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
main
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13303b247e |
v0.3.9 milestone (#283)
* Run the end-to-end suite on pull requests into milestone and wip branches
A release's work is now collected on a milestone branch (milestone/v0.4.0)
before it reaches main, and the suite only ran on pull requests into main,
so nothing proposed for the milestone was tested. `milestone/**` joins
`main`, and `wip/**` comes along from wip/28, where it already runs the
suite on the phases of #28 — the same block on both, so they merge
without a conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
67c3b52376 |
fix(compose): a cancelled send no longer reopens a window you closed
In the held send shape the composer deliberately stays on screen while a send is out, so every second of that is a second the close button is live. Cancel the send, then press Save draft and close, and the undo response — which answers with the composer PUT BACK, because that is its job — replaces the frame you just emptied. The window returns by itself and nothing takes it away again. Reproduced deterministically by holding the undo response and closing underneath it, rather than waiting for a full-suite run to be slow enough. Fixed on the client because it cannot be fixed anywhere else: the close is entirely client-side in this state — saveDraft() returns early while sending — so the server never learns the window is gone, and the response is already being generated. A cancelled send now drops only the stream aimed at this window's own frame; the inline variant still takes the sent mail back out of the conversation, which is on screen and genuinely wrong until it runs. Nothing is lost by dropping the reopen: the send has already been called off server-side and the message is a draft again, which is what the reader asked for. The optimistic path is untouched on purpose — there the composer is already closed and the toast's Undo SHOULD bring it back. Also: chromium-exclusive now actually runs on one worker, which its own comment has claimed since it was written. Without the override it inherited four, and `fullyParallel: false` means parallel across FILES — so every spec in a project that exists because its members own install-wide state was running concurrently with the others. It held by luck until two of them wanted the same singleton. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
377d6026b8 |
feat(ai): make the language model observable, and the composer honest
"I click the AI button in the composer and nothing happens" turned out not to be about the model at all. #editor() queried data-compose--toolbar-target while the composer renders data-compose--compose-toolbar-target, so run() returned at its first guard: no request, no status, no error. The feature had never worked once. #subject() was wrong the same way and quieter — every prompt went out with an empty subject. And the status element lived INSIDE the dropdown that the same click closes, so even once working it would have written into a hidden subtree. The rest of this is what makes the next such thing findable. RECORDING One row per model call, tagged by the workload that caused it, and nothing about what was in it — no prompt, no completion, no message id, no subject, no address. error_kind is a category from a closed set for the same reason: an HTTP client's exception message routinely quotes the request body back, and here that is somebody's mail. OllamaClient was discarding Ollama's whole timing block; it now returns it, and AiAssistant — the only place that knows both the feature and the model — writes the row. DBAL and deliberately no entity, following EmbeddingStore: BackfillEmbeddings clears the EntityManager after every chunk, so an unflushed ORM row would be detached and silently dropped, fifty at a time, for as long as a backfill runs — which is exactly the window the panel exists to show. AiFeature could not carry this. A four-character search query somebody is waiting on and two thousand characters of mail body written unattended a hundred thousand times both went through embed() with no discriminator, and averaged together the second buries the first — the panel would report backfill throughput under the heading "search". AiCallFeature splits them. THE PANEL Live state from /api/ps, which is passive and safe to poll. Six states that look different, and the one that justifies the work is "partly on the CPU": size_vram < size means layers spilled, throughput collapses, and the model still answers correctly — invisible everywhere else, so everything is simply slow forever. THE COMPOSER Tokens stream into a provisional surface and commit in one operation, because hundreds of per-token insertions destroy the undo stack and Ctrl+Z is the first thing anyone tries. Waiting, generating, done and failed are distinct states with reasons: outside the keep-alive window the first thirteen seconds produce nothing while 20 GiB loads, and a featureless spinner for that IS the bug. Cancellation propagates to the host — an abandoned draft otherwise holds the GPU and everything else queues behind it. SEARCH Silent degradation to keyword-only is replaced by the reason it degraded, and rows found by meaning say so. The semantic arm and its join are now matched on model AND width: vectors from different models are not comparable, and after somebody changes the search model every existing vector is dead weight. On a pgvector install comparing different widths raises an error — a 500 on /mail/search caused by an admin dropdown — and on the shipped loop it silently compares the overlap, which is worse. Coverage is stated where results are shown, because semantic search over an 8%-indexed mailbox is a trap. BACKFILL Progress is counted from the database, never accumulated, so a killed worker resumes. It stands aside while anyone is using the AI and comes back on its own: both compete for one integrated GPU, and a batch in flight when somebody presses the button puts them straight back to "nothing happens". Also carries the cron line for app:jobs:reap from the commit before this one. Verified against the real host: reachable, both configured models installed, timings parsed, 1024-dimension embeddings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cbfca378f8 |
test(e2e): the suite reads the clock the page is rendered on
The v0.1.13 tag went red here, and none of it was the release. WALL CLOCKS Snooze times are computed in the BROWSER, deliberately — the server is never told the session's zone — but the row is rendered server-side in the user's zone, which falls back to Europe/Berlin. The spec then read the hour back with Node's getHours(), which answers in the RUNNER's zone. All three agree on a developer's machine, which is why this passed eighteen consecutive local runs. On a GitHub runner the browser and Node are UTC while the server is still Berlin: the mail came back at 18:00, the row said 20:00, and the assertion reported the wrong TIME being shown when the truth was the wrong CLOCK being read. APP_TIMEZONE now names the zone rendered times are in, in one documented place; the assertion converts into it rather than trusting the process. And playwright.config pins the browser to it, so a menu that offers six in the evening cannot produce a row that says eight — the divergence is an artefact of an unpinned runner and no real user has it, since their browser and their configured zone are the same place. Reproduced before fixing: reverting the conversion under TZ=UTC fails with the CI failure exactly. TWO FIXTURES LEFT TO CHANCE, both found by running under TZ=UTC at CI's two workers. calendar.spec seeds calendars and never seeded a mailbox, but its pane cases assert on the mail list beside the calendar. Those rows were whatever the last spec on the worker left: bulk-select-all archives two hundred messages and then clears them, after which `[data-row-layout]` matches nothing and the failure reads as a broken container query when there is simply no row to lay out. admin-panels assumed the Processes panel starts open. Collapse is stored per admin user, the admin is shared rather than per worker, and a run that fails anywhere after the first toggle leaves it collapsed — so the next run failed on its opening assertion, reporting a broken feature and really reporting its own earlier failure. What collapsed it this time is not established; the test establishes its own precondition now, which is the part that was missing either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0c3d690a91 |
fix(test): the e2e base URL has one definition, and the suite runs off a clean checkout
preview-pane.spec.ts builds its own browser context — the wizard step needs a separate user and a separate session — and handed it `process.env.E2E_BASE_URL` raw, without the fallback playwright.config.ts applies. Unset, that is `undefined`, and every case in the file died on "Cannot navigate to invalid URL". Which is to say: `npx playwright test` on a stock checkout failed there and nowhere else, while every other spec ran fine off the default, so the failure read as a broken test rather than a missing variable. BASE_URL now lives in tests/e2e/support/config.ts and both the config and the spec import it. `use.baseURL` only reaches contexts Playwright makes itself, so any spec building its own has to be handed the value — and now there is one value to hand it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 1a6d8223403c904ad1e0654e97c6a8cc9fd12f80) (cherry picked from commit 29ac828383ef69aecea484f69b9642d260ffcc45) (cherry picked from commit 4aae6da96e2b8395931b40766fd3cdfde064a0c0) |
||
|
|
56606f2c78 |
ci(e2e): the suite runs against the FrankenPHP the app ships on
CI served the browser suite with `php -S` on the runner, behind a router script written for the occasion, with PHP and its extensions installed beside it. Every accommodation in that workflow existed because of that server and not because of the app: a php.ini copied into the scan dir with sudo, PHP_CLI_SERVER_WORKERS, a router so index.php did not answer 56 asset requests with 56 kernel boots, a copy of the environment into $_SERVER because the cli-server SAPI provides none, and finally a restart loop because the thing segfaulted mid-suite. A green run proved something about a server no user will ever meet, while PHPUnit ran bare on the runner in the same job that every developer ran in a container. It now boots compose.test.yaml — the app's own image, from this repository's Dockerfile — and runs `npm run test:unit:docker` and the Playwright suite against it. Local and CI are one invocation. Three things had to be true first. app-init could not bootstrap a checkout that had never seen composer. It overrides the image entrypoint to get the ordering right around the database, and that entrypoint is also what installs dependencies into the bind mount, so a fresh clone — or a worktree, or a CI runner — died on "Dependencies are missing" before the stack came up at all. It installs them itself now when vendor/ is absent. admin-queue.spec.ts and mercure.spec.ts each carried a private copy of `docker compose -f compose.test.yaml` with no -p, so both silently addressed the DEFAULT project. Point the suite at a stack under any other project name and the queue seed populated a database the browser was not looking at, while the hub stop that mercure.spec.ts uses to prove the stream recovers stopped a project that did not exist — which compose reports as success. That is the whole of why admin-queue was believed to need a running worker: it never did. `delivered_at` is what "a worker is holding it" means to the transport, and app:test:seed-queue writes it. Both now go through E2E_COMPOSE, resolved once in playwright.config.ts. The image needed a stable tag so CI could build it ahead of time with a layer cache, which is why compose.test.yaml names it rather than leaving it to `<project>-<service>`. Deleted: the webServer block, tests/e2e/support/router.php, the Postgres service container, setup-php and its extension cache, the php.ini scan-dir step, and the separate migrate/seed/asset steps the stack already does. The server log still goes up with the report — from `docker compose logs` now, and covering app-init and Postgres too, because when the failure is that the stack never came up that is the only place the reason exists. Verified against a compose stack from this worktree: PHPStan clean, PHPUnit 2820 tests / 6979 assertions green, Playwright 253 passed and 0 failed — admin-queue and both mercure recovery tests among them. |
||
|
|
eab8d650dd |
ci(e2e): the dev server restarts when it dies
php -S segfaulted once mid-suite (v0.0.25) and every later spec failed as ERR_CONNECTION_REFUSED. A crash now costs the requests in flight; their retry finds a listening socket. The log is appended, so the line it died after stays readable above the restart marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
085c4130aa |
Serve the browser suite from PHP, not from the Symfony CLI
103 Playwright tests failed on CI and none of them were broken. The
Symfony CLI detects running Docker containers and injects its own
environment into the app it serves, overriding DATABASE_URL even when one
is explicitly set. On CI the Postgres service is a container, so the URL
the workflow sets was replaced with credentials nothing accepts, the app
could authenticate nobody, the per-worker login fixture died, and every
test in that worker fell over behind it. PHPUnit and every bin/console
step passed in the same job, because none of them go through the CLI.
DATABASE_URL=postgresql://app:app@127.0.0.1:5432/app \
symfony php -r 'echo getenv("DATABASE_URL");'
SYMFONY_SKIP_DOCKER_COMPOSE, SYMFONY_SKIP_DOCKER_SERVICES and
SYMFONY_DOCKER_ENV were all tried; none of them stop it, and there is no
--no-docker flag. So the suite now starts PHP's own server, which has no
detection to disable. The CLI install goes with it: it was there for
`symfony serve` alone.
Two things that server needs and does not do by itself. Naming
public/index.php as the router makes index.php answer everything,
including the 56 compiled assets /login pulls in — each one a kernel boot
returning 404 — so tests/e2e/support/router.php returns false for real
files and lets the server serve them off disk. And the cli-server SAPI
builds $_SERVER from the HTTP request and stops: getenv() answers,
$_SERVER does not, and Dotenv reads $_SERVER. Without the bridge in that
router, DATABASE_URL would silently revert to the committed default in
.env — the same failure as above arriving by a different road. HTTP_* is
excluded from the bridge, because $_SERVER['HTTP_PROXY'] is
indistinguishable from a Proxy header once a Request is built.
The server's log is redirected to a file and uploaded with the report
rather than piped to the console: it writes three lines per request, and
one slice of the suite produced 67,709 of them, which is how a real
failure gets buried. It is still the only place a PHP fatal that never
reached the kernel is recorded, so it is kept rather than silenced.
One onboarding test now retries opening the user menu. The menu is opened
by a Stimulus controller, and Playwright's actionability checks say
nothing about whether a controller has connected — the button is visible
in the server's HTML long before the module graph that animates it
arrives, and a click that lands in that gap is simply lost. FrankenPHP
served those 56 assets fast enough to hide it; this server does not.
Verified against a CI-shaped stack — host PHP, Postgres in a container,
the workflow's env block, this config starting the server: 185 passed,
14 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f111297afd |
Make live updates survive the hub going away, and say when they have not
Three faults, one story: the stream could die silently, nothing told the
user, and nothing tested any of it.
**It did not recover.** EventSource retries on its own only when a
connection drops cleanly. An HTTP error response is fatal per spec —
readyState goes to CLOSED and the browser never tries again — and our
Caddy answers 502 for the seconds a hub restart takes. Measured: an open
stream, a 20s outage, and the tab stayed dead after the hub was back.
Every deploy did this to every open tab. There is now a reconnect loop
with jittered backoff, plus an immediate retry when a tab is refocused
or the network returns.
**Nothing said so.** The mail list has no polling fallback, so a dead
stream looks exactly like a quiet mailbox — the inbox stops changing and
nothing on screen explains why. A glow behind the logo now carries the
state, quiet when healthy, and announced rather than colour-only. While
the stream is down the list falls back to a 60s poll: strictly the worse
mechanism, which is why it runs only when the good one is unavailable.
**The hub accepted anyone.** dev.Caddyfile was in use in every
environment including TrueNAS, and it sets `anonymous`. Since
/.well-known/mercure* is proxied publicly, anyone who could reach an
instance could subscribe to mail/user/<any id> and watch that account's
sync activity. Both stacks now run the image's default config, which
requires the subscriber JWT.
That JWT expires — an hour by default, well inside a remember-me
session — and the hub only checks it when a connection is made, so the
expiry is invisible until the next reconnect is refused. /mercure/auth
re-issues the cookie, and the client refreshes it before every retry.
The cookie is attached by a response subscriber rather than by
`mercure(topics, {subscribe: …})` in the layout, because that helper
throws for a hub on another second-level domain and a throw in a
template takes the whole page with it. PHPUnit caught it at once: its
client requests as localhost against a 127.0.0.1 hub, and every
page-rendering test turned 500. An external hub is a legitimate
deployment; it should cost live updates, not the page.
Tests: compose.test.yaml grew a real hub, running the same non-anonymous
config as production so the suite exercises the authorization path
instead of one that accepts anything. The recovery spec stops the
container — `setOffline` does not sever an established SSE connection
and route interception never fires without a request, so both looked
like passing tests of nothing. It lives in the exclusive project: there
is one hub, and taking it away mid-run breaks specs that never asked.
Verified by disabling the reconnect and watching the recovery test fail,
then restoring it and watching it pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7ec1f56ee7 | test speedup | ||
|
|
d0685d0ee6 | update | ||
|
|
517bd5d345 | testing |