| D-M4-34-0 |
2026-10-25 |
Branch feat/M4-34-desktop-e2e-perf on origin/main. Names that differ from the brief: the world is prepared by the Go probe (bothy-smoke e2e-world, services/server/test/smoke/e2eworld.go) instead of shell calls, because the public API needs DPoP proofs, step-up and ETags that the probe of M1-73 already signs; the Fake-Agent driver is tools/ci/desktop_e2e_agent.py; the suite is apps/client/e2e_test/desktop_e2e_test.dart with one file per area under e2e_test/flows/, the runner of both targets apps/client/tool/e2e_suite.sh. |
Brief Boundaries; Design note 1. |
| D-M4-34-1 |
2026-10-25 |
Shared Compose functions: e2e.sh copies the few helpers of smoke.sh it needs (preflight, sanitized environment, waits, cleanup) and leaves smoke.sh unchanged. The stack is compose.yaml unchanged plus test/compose.smoke.yaml plus test/compose.e2e.yaml: a Caddy TLS front in the network namespace of api on a free loopback port (the server accepts plaintext only from loopback, SEC-TLS-1), a test CA per run whose key is deleted once the leaf is signed, the world job and the Fake-Agent in the same namespace. The overlay turns off the file scan and raises the rate limits of every class for api and worker; every key is in the configuration reference (a test holds that). |
Brief Design note 1; SEC-TLS-1; the suite measures the app, not the limiter (the first runs hit 429 on login and sends). |
| D-M4-34-2 |
2026-10-25 |
Second person and second device: Sam and the owner act through the public API from the test process (ApiActor, its own DPoP keys and a fresh TOTP step), outside the app's network, so cutting the app's network never cuts them; the second device of Alex is a second app instance in the same test (E2eSecondApp). Enrollment codes are single use and issuing one ends the other unused codes of the person, so the suite issues a code per login through the owner. The app reaches the stack through an in-process CONNECT tunnel that the offline flow cuts and reopens, and trusts only the test CA of the run. |
Brief Design note 2 (the API for the second person); one app process per platform runner. |
| D-M4-34-3 |
2026-10-25 |
Notification flow: the post is created by the owner, Alex sets the thread level to all and hides the window, Sam posts until a toast with the tag thread:<post> arrives; the flow asserts the tag, a route payload /t/<post> and a title, not the excerpt. Two server gaps make a single post unreliable: a hint can be lost while the app's presence is not yet flushed (no_route), and a hint applied on another node carries no preview fields (names, excerpt). Both are reported, not fixed here. |
Brief Design note 2; the gaps are in the server's notify path, outside the scope of the ticket. |
| D-M4-34-4 |
2026-10-25 |
Two product defects the suite found are fixed with tests: the history of a thread the device never received (the main thread of a new text channel) now loads the thread once (ThreadReads.loadThread, sync engine loadThread), and the sync engine goes live when a ready with cursor or a resumed names a head equal to the cursor, because the server sends no catchup.complete for an empty catch-up. |
Reproduced in the E2E runs (blank channel, a resume stuck in catching up); 15 6 and 7. |
| D-M4-34-5 |
2026-10-25 |
Measurement points: cold start from the start of the app on a filled data folder (the end of the app before is not counted) until the sidebar shows the space; thread open from the route until the first message row; local echo from the tap on the send button until the row of the message is on screen (waiting or already confirmed); these three end with the first drawn frame that shows the result (a persistent frame callback), not at the next poll of the wait, whose 50 ms step would be as long as the echo target; patch to frame from the arrival of a run.updated frame (Timeline.now in the recording transport) until the first frame that shows the line; jank ratio as frames over one refresh interval of the display while scrolling thousands of messages, from FrameTiming (the budget of the app's own counter client.frames.over_budget, so a 120 Hz panel measures against 8.3 ms; the log names the rate); catch-up rate as events applied per second after a reconnect with a backlog Sam writes while the app is offline; memory as the resident size with twenty threads in focus. p95 is the nearest rank over at least twenty samples; a target with fewer samples fails. |
Brief Design note 3; 15 19. |
| D-M4-34-6 |
2026-10-25 |
Device marking: summary.json names the device (PERF_DEVICE_LABEL) and reference is true only with PERF_REFERENCE=1; numbers from another device are kept and marked. |
Brief Design note 4. |
| D-M4-34-7 |
2026-10-25 |
Where it runs: make e2e-desktop and make perf-desktop on a desktop with a Docker engine, before a release and at the close of M4. GitHub-hosted macOS runners have no Docker engine and Windows runners run only Windows containers, so the workflow e2e-desktop.yml (weekly and manual) analyzes and builds both suites on macOS and Windows and does not run them. On a desktop the suite runs through flutter drive with test_driver/e2e_driver.dart (flutter test runs a file outside integration_test/ on flutter-tester whatever device is named); E2E_DEVICE=flutter-tester runs it headless. |
Brief Design note 6. |
| D-M4-34-8 |
2026-10-25 |
patrol is not adopted: the suite uses integration_test on macOS as on Windows, so it covers no system dialog (keychain, notification permission). patrol on macOS needs a RunnerUITests target in the Xcode project and the UI automation permission on the runner, and the build host has no Xcode. Adopting it is a follow-up for a Mac runner with a Docker engine. |
Brief fixed requirement 12, Acceptance ("or the report says so"); PLAN 10 Risks. |
| D-M4-34-9 |
2026-10-25 |
Desktop builds: the app harness registers the test text input and the flows press keys with their physical key (a build on a device has neither by default); the performance level sends with the composer's send button, because a simulated Enter does not reach the composer in a profile build. |
Observed on macOS with flutter drive and flutter drive --profile. |