Files
kandev/apps/web/e2e
Daniel Brown c0eaafaf29 fix(mobile): isPassthroughMode falls back to TaskChatPanel when snapshot is missing (closes #1031) (#1034)
* fix(mobile): passthrough TUI rendering + touch-scroll

Two related mobile-UI fixes for cli_passthrough sessions, both
filed in #1031.

1. task-center-panel.tsx: align isPassthroughMode with the rest
   of the codebase by checking session.is_passthrough first. The
   memo previously only consulted agent_profile_snapshot.cli_passthrough,
   which can be undefined in the Redux store on re-mount (the snapshot
   isn't always present on the leaner session-list responses). When
   that happened on mobile, the chat panel rendered the persisted-
   messages TaskChatPanel fallback instead of the live PassthroughTerminal.
   Desktop was immune because dockview-desktop-layout.tsx already uses
   the session.is_passthrough field directly. is_passthrough is the
   source of truth used by manager_startup.go's StartAgentProcess
   routing — keeping the frontend consistent with that invariant.

2. passthrough-terminal.tsx: wire touch-drag scrolling on the xterm
   viewport. xterm.js handles mouse wheel natively but not touch
   gestures, so swiping inside the terminal on mobile bubbled up
   and scrolled the page instead of the terminal scrollback —
   effectively making everything that scrolled off-screen unreachable
   on mobile. The new hook attaches touchstart/touchmove/touchend
   handlers that map vertical drag delta to term.scrollLines(),
   gated on (pointer: coarse) so desktop xterm behaviour stays
   identical. Row-height comes from xterm's internal render dimensions
   with a measured fallback (xterm 5.x layout) and a 16px constant
   fallback if both fail.

Closes #1031

* fix(mobile): also fix useSessionLayoutState's isPassthroughMode + tighten touch-handling

Three follow-on fixes after Dan's mobile QA surfaced more cases:

1. apps/web/hooks/use-session-layout-state.ts — the mobile layout actually
   uses THIS hook's isPassthroughMode memo (not the one in
   task-center-panel.tsx that I previously fixed). It had the identical
   snapshot-only bug. Aligned with the same is_passthrough-first priority.

2. apps/web/components/task/passthrough-terminal.tsx — always preventDefault
   on touchmove (was only doing so when the row-delta calculation rounded
   to non-zero, so sub-row drags slipped through to parent scroll). Added
   touch-action: none + overscroll-behavior: contain inline on the terminal
   element so the browser never claims the gesture.

3. apps/web/app/globals.css — overscroll-behavior: none on body to block
   pull-to-refresh. iOS Safari ignores this in some configurations but
   blocking it where possible reduces the page-reload race that was
   triggering the stuck-on-loading-messages state.

* fix(mobile): trim PR to minimum required changes

After QA showed the user-visible mobile bug is fully addressed by the
useSessionLayoutState memo fix alone, revert the parallel changes to
the desktop hook (task-center-panel.tsx's useSessionApprove) and the
body-level overscroll-behavior — neither was required for the
reported bug, and Dan asked for the smallest possible diff.

Remaining changes:
- apps/web/hooks/use-session-layout-state.ts — actual mobile fix
- apps/web/components/task/passthrough-terminal.tsx — touch-scroll feature

* fix(mobile): tighten isPassthroughMode + drop terminal touch-scroll from this PR

Adversarial-review feedback applied:
- Change `is_passthrough === true` to `!== undefined` so an explicit
  server-side false isn't overridden by a stale snapshot.
- Comment now cites the specific WS handler quirk in
  ws/handlers/agent-session.ts where snapshot writes are truthy-guarded
  but is_passthrough writes are !== undefined-guarded — that's the
  concrete path where partial events land one without the other.

Dropped: the useMobileTouchScroll hook. Terminal scroll on mobile is a
separate feature that deserves its own discussion (xterm renders the
canvas above the scrollable viewport so native scroll doesn't reach the
viewport; the right fix likely lives in xterm itself or a small
event-redirect addon). Keeping this PR scoped to the rendering bug.

* fix(mobile): add minimal touch-scroll fallback for xterm viewport

xterm.js renders .xterm-screen (canvas) above .xterm-viewport, so iOS /
Android native scroll on the viewport never sees the touches and the
terminal is unscrollable on mobile.

useTouchScrollFallback wires touchstart/touchmove/touchend to
Terminal.scrollLines via an accumulated fractional-row delta. No
inertia — the proper fix is upstream in xterm-js. Gated on
(pointer: coarse) so desktop xterm behaviour is unchanged.

Sets touch-action: none + overscroll-behavior: contain inline on the
terminal element so the browser doesn't claim the gesture for
pull-to-refresh.

* fix(mobile): inertia decay on touch-scroll release for native-scroll feel

* fix(mobile): address adversarial review of touch-scroll hook

- Read xterm from ref inside each handler so renderer recreate doesn't
  strand the handlers on a stale Terminal instance (React refs are
  stable so dep array doesn't catch .current swaps).
- Drop the internal _core._renderService private-API path for row
  height in favor of public term.rows + measured element height. The
  internal path was API-unstable across xterm majors.
- Velocity window 80ms → 50ms so a direction reversal at the tail of
  the gesture is the release velocity, not averaged against the early
  motion.
- Reset pendingRows fractional accumulator on direction reversal — was
  causing perceptible stickiness when reversing from slow-up to fast-down.
- Style restoration now uses removeProperty when the inline style was
  never set, rather than leaking an empty string in its place.

* fix(mobile): revert touch-scroll hook from this PR — separate concern

After several iterations the touch-scroll fallback kept surfacing new
issues (iOS pull-to-refresh leaking through preventDefault, scroll
direction reversal edge cases) that suggest a more involved upstream
discussion is needed about whether to ship a workaround in
PassthroughTerminal at all vs fixing xterm.js's mobile story upstream.

Splitting that work out so this PR can land the rendering bug fix
cleanly. Will file a separate issue for the touch-scroll work with
the diagnosis + alternatives we explored.

* fix(mobile): honor is_passthrough when snapshot missing

---------

Co-authored-by: Carlos Florêncio <carlosmflorencio@gmail.com>
2026-05-25 22:09:17 +01:00
..
2026-05-25 10:04:40 +01:00
2026-03-02 08:54:58 +00:00
2026-05-25 10:04:40 +01:00

Kandev Web — E2E Test Suite

Playwright-based end-to-end tests. Each Playwright worker spawns its own real Go backend (no mocks of internal services) on isolated ports and a real Next.js standalone frontend, then drives a real Chromium against them.

Project layout

Folder What's in it
tests/ Spec files, grouped by feature (chat/, docker/, git/, integrations/, kanban/, pr/, search/, session/, ssh/, layout/, …).
fixtures/ Worker-scoped fixtures that own the backend lifecycle (backend.ts, docker-test-base.ts, ssh-test-base.ts, test-base.ts).
helpers/ Reusable building blocks for specs (api-client.ts, docker.ts, ssh.ts, git-helper.ts, ws-capture.ts, …).
pages/ Page Objects (one class per top-level UI surface — SessionPage, KanbanPage, JiraSettingsPage, SSHSettingsPage, …).
playwright.config.ts Project definitions, timeouts, sharding config.
global-setup.ts Pre-flight checks for required binaries (kandev, mock-agent, standalone next build).

Playwright projects

The suite is split into three projects. Pick one with --project=<name>.

chromium (default)

The everyday surface — runs in every CI shard. Excludes the heavyweight containers specs and the mobile specs.

pnpm e2e

mobile-chrome

Same as chromium but on Playwright's Pixel-5 viewport, gated on tests/**/mobile-*.spec.ts. Runs in the same CI shard matrix as chromium.

containers — Docker required

This is the "real-infra heavyweight" project. Despite the name, it covers more than just the Docker executor — it's where any test that needs Docker on the host as a runtime lives:

  • Docker executor tests (tests/docker/*.spec.ts) — verify kandev launches real kandev-agent:e2e containers, recovers them across backend restarts, cleans them up on archive/delete, etc.
  • SSH executor tests (tests/ssh/*.spec.ts) — verify kandev SSHes into a real kandev-sshd:e2e container, uploads agentctl, runs an agent end-to-end, recovers across backend restarts, etc. The SSH executor's remote target is a Docker container in tests, even though the SSH connection itself is a real SSH connection.

This project:

  • Skips entirely when no Docker daemon is reachable. Contributors without Docker can still run chromium + mobile-chrome.
  • Builds container images on demand. First run builds kandev-agent:e2e (slim Node base + git) and kandev-sshd:e2e (Alpine + openssh-server + git + pre-baked mock-agent). Subsequent runs hit Docker's layer cache.
  • Has a longer per-test timeout (180s vs 60s) because container starts + agent setup are slow.

How to run it locally (requires Docker running):

KANDEV_E2E_CONTAINERS=1 pnpm e2e --project=containers

Or a single spec:

KANDEV_E2E_CONTAINERS=1 pnpm e2e --project=containers tests/ssh/launch-task.spec.ts

Why "containers" instead of "docker"?

This project used to be named docker. It was renamed to containers once SSH e2e tests joined it — calling it docker was misleading because SSH tests have nothing to do with the Docker executor; they just happen to use Docker as the runtime that hosts the sshd target.

KANDEV_E2E_DOCKER=1 is still honored as a deprecated alias for KANDEV_E2E_CONTAINERS=1 for one release. Local scripts and stale CI configs won't break, but new code should use the new name.

Commands

Command What it does
pnpm e2e Run the default (chromium) project headless.
pnpm e2e:ui Open Playwright's UI mode for interactive runs.
pnpm e2e:headed Run headless project but with a visible browser.
pnpm e2e --project=containers Run container-backed tests (needs Docker).
pnpm e2e --project=mobile-chrome Run mobile responsive tests.
E2E_DEBUG=1 pnpm e2e Surface Docker build output + extra logging.

Common flags: --shard=1/4, -g "fragment of test name", --repeat-each=3 (flake hunting).

Backend isolation per worker

Every Playwright worker gets:

  • A unique backend port in BACKEND_BASE_PORT + workerIndex (default 18080+).
  • A unique frontend port in FRONTEND_BASE_PORT + workerIndex (default 13000+).
  • A fresh tmpdir (HOME, KANDEV_HOME_DIR, worktree base, repo clone base — all under that tmpdir).
  • A unique agentctl instance port range (30001 + E2E_PORT_OFFSET * 1000 + workerIndex * 200).
  • Its own SQLite DB.

Workers run in parallel across CI shards (--shard=N/M); within a worker, tests run serially because the testPage fixture calls e2eReset on the shared backend before each test.

Mocked vs real

  • Mocked: Jira (KANDEV_MOCK_JIRA=true), Linear (KANDEV_MOCK_LINEAR=true), GitHub (KANDEV_MOCK_GITHUB=true), the agent process itself (KANDEV_MOCK_AGENT=only). These are third-party services or external processes we don't want CI to depend on.
  • Real: Everything inside the kandev backend — orchestrator, lifecycle manager, agentctl, SSH/SFTP, Docker SDK, git, worktree manager. The point of e2e is to exercise the real code paths.

The SSH executor specifically has no mock controller. Tests use a real Docker-hosted sshd as the remote target, and fault-injection (host-key rotation, dropped traffic, killed pids) is done by operating on the container itself.

Adding a new spec

  1. Pick a directory under tests/ (or create one for a new feature).
  2. Decide which project it belongs to. Anything that needs Docker → tests/docker/ or tests/ssh/ (lands in containers). Anything mobile-specific → name it mobile-*.spec.ts. Otherwise it joins chromium automatically.
  3. Import the right test base:
    • import { test, expect } from "../../fixtures/test-base"; for normal tests.
    • import { test, expect } from "../../fixtures/docker-test-base"; for Docker executor tests.
    • import { test, expect } from "../../fixtures/ssh-test-base"; for SSH executor tests.
  4. Use getByTestId for selectors. If the surface you're testing lacks stable testids, add them — drift-prone CSS / text selectors are not worth the maintenance cost.

CI

.github/workflows/e2e-tests.yml defines two jobs:

  • e2e — matrixed chromium + mobile-chrome shards.
  • e2e-containers — single job, runs --project=containers, needs Docker.

Both upload blob reports that e2e-report merges into a single HTML artifact.