name: Chat shell gestures # Real-browser gesture + interleaving e2e for the floating chat shell # (packages/ui ContinuousChatOverlay + home screen), the component-tree # render-parity contract, and the CHAT_GESTURE_COVERAGE gate (#12188). # # All 11 __e2e__ runners compose the shared runner toolkit # (packages/ui/src/testing/e2e-runner) and record a stable-named .webm. This lane # drives the REAL surfaces with real pointer/touch gestures in headless chromium, # runs the boot-free coverage gate that fails when a gesture-handler site lacks a # matrix row, and uploads every runner's video + screenshots. # # One shell runner is intentionally NOT in this lane: # - run-fused-wake-integration-e2e drives the REAL native desktop wake producer # (libwakeword + the wakeword-cpp build), which this lane does not compile # (install-native-deps:false) — it self-skips without the lib, so it lives in # the native-desktop build lane. # # run-perf-gate-e2e USED to be excluded here for a CLS 0.80 failure, but that # shift was the conversation-swipe layout thrash: #13531 removed swipe (one # infinite thread) and #13826 retargeted the gate onto scroll + maximize/restore, # so CLS is now 0.0000 on develop (stable across runs, not timing-flaky). It is # re-enabled below (#14333) — CLS is structural so it will not flake on slower # fleet hardware, and the frame budget carries ~3x margin (p95 ~10ms ≤ 33ms). # Both gates self-verify their detector's teeth: each injects a real non-transient # shift and asserts it is flagged, so a 0.0000 can never be a dead-observer pass. # # Path-gated to packages/ui (and the app-side gate + matrix) so it only runs when # the surface it covers changes. The e2e runners bundle the fixtures with esbuild # and stub @elizaos/core, so no core build is needed; the render-parity vitest # imports the real @elizaos/core, so the shared i18n data is generated first. on: pull_request: branches: [main] paths: - ".github/workflows/chat-shell-gestures.yml" - "package.json" - "bun.lock" - "packages/ui/**" - "packages/shared/**" - "packages/core/src/i18n/**" - "packages/app/test/chat-gesture-coverage.test.ts" - "packages/app/docs/CHAT_GESTURE_COVERAGE.md" push: branches: [main, develop] paths: - ".github/workflows/chat-shell-gestures.yml" - "package.json" - "bun.lock" - "packages/ui/**" - "packages/shared/**" - "packages/core/src/i18n/**" - "packages/app/test/chat-gesture-coverage.test.ts" - "packages/app/docs/CHAT_GESTURE_COVERAGE.md" workflow_dispatch: # CI fan-out fix: the PR-scoped concurrency from #14069 fell back to # github.run_id on push, so every develop push got a unique group and # nothing ever superseded. A 37-merge wave queued thousands of push runs # and starved the self-hosted fleet. Fall back to github.ref (all develop # pushes share a group) and cancel superseded runs on push too. PR behavior # is unchanged (pull_request.number still wins the key); merge_group and # schedule events are not in the cancel set, so the merge queue and nightly # runs always complete. concurrency: group: chat-shell-gestures-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} permissions: contents: read env: CI: "true" BUN_VERSION: "canary" NODE_VERSION: "24.15.0" NODE_NO_WARNINGS: "1" jobs: chat-shell-gestures: name: Chat shell gesture + parity e2e runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 40 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install --ignore-scripts --no-frozen-lockfile install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" # The render-parity contract imports the real @elizaos/core, whose i18n # module loads generated keyword data that is gitignored. Generate it so # the vitest run resolves (mirrors the attachment-journey-videos lane). - name: Ensure generated shared i18n data run: node packages/app-core/scripts/ensure-shared-i18n-data.mjs - name: Install Playwright chromium + webkit run: bunx playwright install --with-deps chromium webkit # Boot-free coverage gate: fails when a gesture-handler site in # packages/ui/src lacks a CHAT_GESTURE_COVERAGE matrix row (#12188). - name: Chat gesture coverage gate run: bun run --cwd packages/app test -- chat-gesture-coverage # Component-tree render parity (jsdom/vitest) — ThreadLine vs MessageContent # render the same structure for a shared message corpus. - name: Render-parity contract run: bun run --cwd packages/ui test -- src/components/chat/render-parity.contract.test.tsx # Conversation-nav interleaving fuzz (jsdom) — the pure most-recent-first # invariants across new / select sequences (#10042). Chat-to-chat swipe # was removed by the single-infinite-thread redesign (#13531), so the swipe # e2e + its jank hook were retired in #13826; the remaining overlay gesture # cost is now covered by the maximize/restore perf gate below. - name: Conversation-nav unit + interleaving run: bun run --cwd packages/ui test -- src/components/shell/conversation-nav.test.ts # The iOS-style chat-sheet detent/gesture state machine (mouse + touch). - name: Chat-sheet gesture e2e run: bun run --cwd packages/ui test:chat-sheet-e2e - name: Chat-sheet Safari/WebKit gesture smoke run: bun run --cwd packages/ui test:chat-sheet-safari-e2e # "can't scroll chat on web" repro (#chat-scroll-web): seeds a long # transcript, opens the sheet to FULL, and asserts the transcript scroller # resolves a BOUNDED clientHeight (so there is overflow) and that a real # vertical finger-drag moves scrollTop natively instead of being hijacked # into the sheet pull. RED before the fix, GREEN after. - name: Chat-scroll web e2e run: bun run --cwd packages/ui test:chat-scroll-web-e2e # Transcript is vertical-only (#14328): an over-wide child + diagonal wheel # must not pan #continuous-thread sideways; a designed inner scroller still # scrolls in its own row and does not chain. Both engines — the axis lock is # a *desktop trackpad* deltaX bug and WebKit's scroll semantics differ, so # the acceptance bar requires the assertion to hold on WebKit too. - name: Chat transcript axis-lock e2e (chromium) run: bun run --cwd packages/ui test:chat-scroll-axis-lock-e2e - name: Chat transcript axis-lock e2e (webkit) run: ENGINE=webkit bun run --cwd packages/ui test:chat-scroll-axis-lock-e2e # TopicGroup flick-collapse/expand gestures. - name: TopicGroup gesture e2e run: bun run --cwd packages/ui test:chatux-gesture-e2e # Home↔launcher pager swipe, notification pull, launcher long-press # (real CDP touch on mobile + fine-pointer desktop edge buttons). - name: Home-screen gesture e2e run: bun run --cwd packages/ui test:home-screen-e2e # Seeded launcher long-loop (#12375): 500 real CDP-touch gestures over the # composed home↔launcher surface, every §D invariant checked after each # command, with a full walkthrough video. Pinned seed for a deterministic # gate; fuzz locally by unsetting ELIZA_LOOP_SEED. - name: Launcher gesture long-loop e2e env: ELIZA_LOOP_SEED: "12375" run: bun run --cwd packages/ui test:launcher-loop-e2e # Bottom-bar state-machine gestures. - name: Bottombar gesture e2e run: bun run --cwd packages/ui test:bottombar-e2e # Warm-up 503 eviction: optimistic bubble survives the reconcile reload + # retry-once delivery (records the recovery flow). - name: Warm-up eviction e2e run: bun run --cwd packages/ui test:warmup-eviction-e2e # /chat ambient orange-pulse background: sample the 30s CSS animation # across its named peaks while recording a .webm. - name: Chat ambient pulse e2e run: bun run --cwd packages/ui test:chat-ambient-e2e # Perf gates: drive the overlay under thread-scroll + pull-to-maximize / # top-pull-restore, feed real PerformanceObserver + rAF entries into the # shared frame-budget + layout-stability detectors, hard-fail on dropped- # frame % / p95 / CLS. Each gate first proves its CLS detector has teeth by # injecting a real non-transient shift and asserting it is flagged (#14333), # so a true CLS of 0.0000 cannot be a vacuous pass from a dead observer. - name: Chat overlay perf gate run: bun run --cwd packages/ui test:chat-perf-gate # Scroll + maximize/restore perf gate over the REAL overlay (#9954 item 5, # retargeted off the removed swipe path by #13826). Hard-fails on # dropped-frame % / p95 frame time / CLS ≤ 0.1. Re-enabled in this lane by # #14333 now that the swipe-CLS 0.80 is gone (CLS 0.0000 on develop). - name: Chat overlay scroll/maximize perf gate run: bun run --cwd packages/ui test:perf-gate-e2e # #9142 Part 2: capture a dense CDP frame burst across the pill→open spring # (animations ON) and run the single-frame-flash + two-pills-crossfade # detectors. The harness was merged (#10356/#10374) but ran in NO workflow, # so a transient glitch could still ship. Run under bun (resolves the # esbuild/pixelmatch/pngjs deps directly, regardless of the node-run # portability fallback in #10400). - name: Frame-glitch (transient) e2e run: bun packages/ui/src/components/shell/__e2e__/run-chat-sheet-frame-glitch-e2e.mjs # Self-test: --canary injects a one-frame wrong state and asserts the # detectors FIRE on it; the harness exits 0 iff they do (and non-zero if it # has gone blind). This guards against the detectors silently degrading. - name: Frame-glitch canary self-test run: bun packages/ui/src/components/shell/__e2e__/run-chat-sheet-frame-glitch-e2e.mjs --canary - name: Upload gesture videos + screenshots if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: chat-shell-gesture-artifacts path: | packages/ui/src/components/shell/__e2e__/output/ packages/ui/src/components/shell/__e2e__/output-webkit/ packages/ui/src/components/shell/__e2e__/output-chatux/ packages/ui/src/components/shell/__e2e__/output-home/ packages/ui/src/components/shell/__e2e__/output-launcher-loop/ packages/ui/src/components/shell/__e2e__/output-bottombar/ packages/ui/src/components/shell/__e2e__/output-warmup-eviction/ packages/ui/src/components/shell/__e2e__/output-perf-gate/ packages/ui/src/components/shell/__e2e__/output-frame-glitch/ if-no-files-found: warn retention-days: 14