M5-13: Terminal E2E on mobile¶
Status: ready | Depends on: M4-38, M5-11, M5-12 | Milestone: M5
Goal¶
After this ticket the terminal on the phones is tested end to end on devices against the Compose stack with a host service container (exit criterion 4 of M5): make e2e-mobile of M5-11 gains a terminal suite that pairs a host service container through the server's API (as an owner would on the desktop), assigns Alex a session user, and then on the iOS simulator and the Android emulator opens a terminal with the step-up device key and the simulated biometric match, runs a long command and ends it with Ctrl+C through the extra keys bar, cuts the device's network and reattaches after it returns with the scrollback replayed, moves the app to the background and checks that the app switcher image shows no terminal content, and checks the reattach within and after terminal.quick_reattach_window. With a physical reference device attached, the suite also measures client.perf.terminal_frame_to_paint_p95 and client.perf.terminal_throughput_min on the phone with the output generator of the host service test image and the evaluation of M5-11 (15 19 lists them without a platform limit); without one, the evaluation runs on recorded summaries and the targets are listed as not proven. The accessibility checklist of M5-10 gains the rows for M8 and M9, checked with VoiceOver and TalkBack where the tools allow it and by machine-checkable tests for the rest (the line at the cursor readable, running output not announced, labels of the bar, font size). Why: the red team of M5 checks the terminal on a stolen unlocked phone, in the app switcher and under network loss (PLAN 11), and only an end-to-end run with a real host service shows the parts working together.
Read first¶
docs/PLAN.md: section 11 (Exit criteria 4 and 5, Test strategy "Terminal", Red team "Terminal"), rows M5-11, M5-12, M5-13; section 10 row M4-38 (desktop terminal E2E: pairing in the test, output generator).docs/spec/24-terminal.md: 3.2 (pairing), 6.1, 6.2 (detach, reattach, scrollback, quick reattach), 10.3, 12 (row Mobile), 14 (TM-T12, TM-T16 for network loss).docs/spec/15-client-architecture.md: 23.2, 23.5 (row "E2E": devices).docs/spec/01-product.md: 5.3 M8, M9; 8 (row "Terminal").docs/quality/accessibility-mobile.md(checklist of M5-10).docs/DECISIONS.md: D-W-100 and its extension to M5 and M6 (D-W-120); section 10 (format ofD-M5-13-<n>rows) and the rows of M5-10, M5-11, M5-12.- Briefs (merged names win, D-W-100):
M5-11-mobile-e2e-perf.md(runner, overlay, suites, CI),M5-12-mobile-terminal.md(screens, bar, lifecycle, capture handling),M5-10-mobile-a11y.md(checklist format),M2-48-terminal-conformance.md(host service container and test overlay),M2-41-host-pairing.md(pairing through the API),M4-38-terminal-e2e-perf.md(the output generator in the host image and the timing hook behindTERMINAL_PERF_HOOKS, reused here). - Code on the base branch:
infra/compose/(E2E overlay of M5-11, host service overlay of M2-48),apps/client/e2e_test/phone/,tools/e2e/.
This ticket touches no API operation, no table, no action, no event and no configuration key.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names; the host in the test is named "Build host" as in 01. - Windows only through named configuration keys (
terminal.quick_reattach_window,terminal.detach_graceset short in the E2E configuration); code comments name the key, never the number. - Specification first: a change to behaviour updates
docs/spec/in the same change. The core contract is not edited by this ticket.
Product requirements implemented here:
patrolon iOS and Android against the Compose stack with a host service container: opening with biometrics, Ctrl+C through the bar ends a running command, network loss and reattaching with scrollback, app switcher image without terminal content (PLAN row).- Reattach within
terminal.quick_reattach_windowwithout step-up and after it with step-up, on the real server (TM-T12 from the device). - The checklist with VoiceOver and TalkBack for M8 and M9 is complete; machine-checkable items are tests.
Scope¶
Create:
apps/client/e2e_test/phone/terminal_e2e_test.dart(next to the suites of M5-11, outsideintegration_test/): the suite of Design note 2.apps/client/test/a11y/terminal_a11y_test.dartand the platform audit entries for M8 and M9 (Design note 3).
Change:
infra/compose/test/compose.e2e-mobile.yaml(M5-11): include the host service container of the M2-48 test overlay, a session user for Alex, and the short terminal windows; the seed pairs the host through the API (pairing code, fingerprint confirmation, LLM loginnone, session user assignment) or reuses the pairing helper of M2-48.tools/e2e/runner: the terminal suite in the mobile run; the host container's lifecycle under the same teardown rule..github/workflows/e2e-mobile.yml: the Android job runs the terminal suite too.docs/quality/accessibility-mobile.md: rows for M8 and M9.apps/client/README.md,docs/spec/15-client-architecture.md23.5 (row "E2E" for devices), rootCHANGELOG.mdunder[Unreleased],docs/DECISIONS.mdsection 10 rowsD-M5-13-1toD-M5-13-3.
Out of scope¶
- The console terminal (M6-18).
- Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M5-13-mobile-terminal-e2e; one writer per worktree. - Conventional Commits with the component as scope (for example
test(client): ...,build(compose): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules); the stack publishes its ports only on
127.0.0.1with free ports chosen by the runner. - Stop only processes and containers you started, by PID or container ID; simulators by UDID, emulators by serial. Temporary directories only via
mktemp -d, removed at the end. - Never read or print secrets; never log terminal content or keys.
- The run needs Docker, macOS with Xcode for iOS and the Android SDK with an emulator; the ticket must run
make e2e-mobilewith the terminal suite at least once on both platforms on the build host or stop withVerdict: BLOCKED <tool> missing; the report names where it ran. - Never lower a threshold, disable a gate, delete a test, raise
repo.max_file_kbor extendquality/licenses/allowed.txt. - If the core contract or
docs/api/lacks something this ticket needs, stop withVerdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M5 and M6 by D-W-120). Where the merged code of an earlier ticket names a package, type, function, file, provider or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Host in the stack (24 3.2; M2-48; Ruling for docs/DECISIONS.md). The host service container joins the Compose network of the E2E overlay and connects over TLS to the sidecar of M5-11 Design note 6 under the name
api(the certificate nameslocalhostandapi; the host trusts the test CA), never in plaintext (24 2.2, SEC-TLS-1). The pairing declares the host as the server host (the host itself does not detect it, becauseapiis not its own address; do not assertreported_is_server_host), and only owners may open terminals on it (SR10); Alex is the instance owner in the M5-11 seed, so the owner path is what the suite tests; the seed runs the pairing through the API with an owner session (code,bothy-host pairinside the container with the code, fingerprint confirmation with the first characters) and assigns Alex the session user the container reports. Create the host withassigned_user_idset to Alex and grantterminalon the host to Alex (24 8); as owner on the server host Alex passes SR10, and a test that a non-owner (Sam, given aterminalgrant on the host and a fresh step-up with the device key, so that visibility and step-up pass first) is refused there withterminal_deniedserver_host_owner_onlycovers the other side; the network cut of the suite affects only the device's path, never the host container's. -
Suite (PLAN row). On each platform: log in as Alex (M5-11 setup); open M9, see "Build host" online; open a
shellsession with the step-up device key and the simulated biometric match; typeecho e2e-terminaland see it; startsleepwith a long duration, tap Ctrl thencon the bar, see the prompt return; cut the network (as M5-11 does), wait less thanterminal.detach_grace, restore it, see "detached" and then the reattach with the earlier output replayed; send the app to the background and back withinterminal.quick_reattach_windowand see no step-up prompt, then again after the window and see the prompt; during the background state assert the privacy cover (iOS) orFLAG_SECURE(Android) and take the system snapshot wherepatrolcan, asserting that no terminal text appears in it. -
Accessibility (01 8 row "Terminal"; components.md 12; M5-10 format). Tests: the line at the cursor is exposed to semantics; a command "read screen" exposes the visible screen; running output does not trigger announcements; the bar keys have labels including the latched state; the terminal font follows the system text size and the separate terminal size setting. The platform audits of M5-10 run on M8 and M9. Manual items (a person with VoiceOver and TalkBack on a device) stay "manual before release" in the checklist.
-
Rulings for
docs/DECISIONS.md(D-M5-13-<n>): the host in the E2E stack and its pairing (note 1); the snapshot assertion per platform (note 2); the accessibility checks of M8 and M9 (note 3).
Tests (write them first)¶
terminal_e2e_test.dart(Design note 2), red first before the overlay includes the host.terminal_a11y_test.dartand the audit entries (Design note 3).- Runner tests: the host container is torn down after a failing suite (fake commands).
- The terminal measurement on a physical device (profile build) with the output generator and the timing hook of M4-38 and the evaluation of M5-11 (nothing new is built for them); its evaluation tests on recorded summaries.
Commands that must pass, from the repository root:
make e2e-mobileon the build host with an iOS simulator and an Android emulator (all suites green, including the terminal suite)cd apps/client && flutter analyze --fatal-infos && flutter test --coveragemake runners-check,make lint,make docs,make linkcheckmake check
Acceptance criteria¶
-
patrolon iOS and Android: opening with biometrics, Ctrl+C through the bar ends a running command, network loss and reattaching with scrollback, app switcher image without terminal content. - Reattach within the window without step-up and after it with step-up against the real server.
-
client.perf.terminal_frame_to_paint_p95andclient.perf.terminal_throughput_minare measured on a physical reference phone and a miss failsmake perf, or the report lists them as not proven for the M5 closing (D-W-122). - The checklist with VoiceOver and TalkBack for M8 and M9 is complete.
- Overlay, runner, workflow, README, 15 23.5, CHANGELOG and the rulings are updated.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comments on every public symbol; comments explain why.
apps/client/README.mdand the checklist updated in English.- Rulings of Design note 4 recorded in
docs/DECISIONS.mdsection 10. - Root
CHANGELOG.mdentry under 'Unreleased'. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make checkand ofmake e2e-mobile. - Review and red team verdicts and the findings fixed.
- Where the E2E ran (host, simulator and emulator images).
- What is not proven (for example a physical phone, a hardware keyboard, screen recording on iOS).
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.