Skip to content

test(terminal): run the browser terminal suite in CI, on real Safari, and close the Windows slot leak #259

Description

@zackees

Context

#256 was fixed in #258, but the browser terminal's validation still has holes. Each hole can ship a broken terminal while every check is green.

  1. tests/frontend/test_terminal.py runs in no workflow. None of the 30 files in .github/workflows/ mention it or Playwright. bash test runs the Rust suite and tests/unit/ only. So fix(cli): interactive terminal is dead — the server aborts every WebSocket upgrade task at handshake #255, a complete terminal outage, shipped green, and fix(terminal): a write parked on a full PTY input queue holds its slot until the foreground program exits #256 had to be proven RED -> GREEN by hand. The 16 green checks on fix(terminal): release a session slot when its client disconnects mid-write #258 did not include the terminal suite.
  2. Real Safari has never run the terminal. fix(terminal): release a session slot when its client disconnects mid-write #258 validated Playwright WebKit on Linux, which is Safari's engine but not Safari itself. CLAUDE.md makes Safari a required production target. macos-arm-live-test.yml already enables safaridriver and runs ci/safari_smoke.py, but only against a compiled sketch, not /terminal/ws.
  3. Windows still has the fix(terminal): a write parked on a full PTY input queue holds its slot until the foreground program exits #256 slot leak, confirmed from the code. kernal-api v0.1.11 implements the bounded write only in platform_linux/terminal.rs and platform_macos/terminal.rs. The default trait method in platform/terminal.rs returns ErrorKind::Unsupported. PtySession::write_available in src/pty.rs handles that by falling back to the blocking write, and its own doc comment says "only the interruption guarantee is Unix-only". So on Windows, write_input in crates/fastled-cli/src/terminal.rs can still park, and a disconnect cannot release its semaphore slot. The regression test is skipif(os.name == "nt") because it uses stty, so nothing catches this.
  4. Two design questions from fix(terminal): a write parked on a full PTY input queue holds its slot until the foreground program exits #256 were closed without a decision:
    • Should one client message be capped so it cannot overrun the PTY input queue on its own?
    • Should the whole master be non-blocking, rather than only the bounded write path?
  5. The documented local gate command is stale. docs/interactive-terminal.md (lines ~100-110) points at /tmp/fastled-terminal-shell.nix, which no longer exists, plus a pinned /nix/store Playwright path and a worktree path.

Proposal

Work in this order; each phase can land on its own.

Phase 1: Linux CI job (do first)

Add a job, e.g. linux-x86-terminal-test.yml, for pull requests and pushes to main. It should:

  • build the debug fastled binary through soldr (reuse the _unit-test.yml setup and cache suffix);
  • provide esbuild (via the FastLED toolchain or npm);
  • install Playwright Chromium and WebKit with system deps (playwright install --with-deps chromium webkit on ubuntu-24.04, or run inside mcr.microsoft.com/playwright:v1.62.0-noble);
  • run FASTLED_TERMINAL_BINARY=... FASTLED_ESBUILD=... uv run --with playwright==<pin> pytest tests/frontend/test_terminal.py -v;
  • upload the server log and any Playwright trace on failure.

The test must not skip silently in CI. When CI is set, a missing FASTLED_TERMINAL_BINARY or FASTLED_ESBUILD must fail the run. Otherwise a misconfigured job reports green.

Phase 2: real Safari

Extend the macOS live test (macos-arm-live-test.yml with ci/safari_smoke.py, which already has safaridriver enabled) with a terminal smoke that:

  1. opens the terminal page served by the installed wheel's fastled;
  2. waits for a shell prompt;
  3. types printf 'SAFARI%s\n' 240 and asserts SAFARI240 in the xterm buffer;
  4. runs the four-disconnect blocked-stdin scenario (the slots must all come back quickly) using WebSocket from page JS.

This is WebDriver, not Playwright, since Playwright cannot drive Safari.

Phase 3: Windows bounded write (fix upstream first, then here)

  1. kernal-api: implement write_available for the Windows ConPTY backend, bounded by timeout. One option is a dedicated writer thread per session with a bounded hand-off; another is overlapped I/O on the input pipe. Tag a release.
  2. fastled-wasm: bump kernal-api and kernal-api-build, and add a Windows version of the blocked-stdin test. It should use a foreground program that does not read stdin (e.g. powershell -NoProfile -Command Start-Sleep 30), not stty.
  3. Run it in windows-x86-unit-test.yml or a Windows terminal job.

Phase 4: decide the #256 leftovers

  • Per-message input bound: decide whether the server caps a single client message or splits it into slices below the queue size. It already has a 64 KiB WebSocket message cap.
  • Non-blocking master scope: record whether the rest of the PTY master stays blocking.

Write both decisions into docs/interactive-terminal.md. Only turn them into code if the decision calls for it.

Phase 5: docs

Replace the stale nix-shell gate in docs/interactive-terminal.md with:

  • the CI job as the canonical gate;
  • a portable local recipe: native Playwright where the browsers install, or a Docker playwright run-server on hosts that cannot install WebKit (NixOS).

Recipe verified on NixOS for #258:

docker run -d --name pw-webkit --network host --init mcr.microsoft.com/playwright:v1.62.0-noble \
  /bin/sh -c "cd /tmp && npx -y playwright@1.62.0 run-server --port 39123 --host 127.0.0.1"

Then make WebKit launches connect to ws://127.0.0.1:39123/, with the client at playwright==1.62.0, since client and server minor versions must match. For #258 that was a small pytest plugin patching BrowserType.launch; a first-class env var such as FASTLED_PLAYWRIGHT_WEBKIT_ENDPOINT handled in the test file would be cleaner.

Acceptance criteria

  1. RED -> GREEN for phase 1. In a throwaway commit on the PR branch, revert the write_input change in crates/fastled-cli/src/terminal.rs (the fix(terminal): release a session slot when its client disconnects mid-write #258 fix). The new CI job must fail on test_terminal_240_disconnect_with_blocked_stdin[chromium] and [webkit] with "leaked terminal slots". Restore the fix and the job goes green. Link both run URLs in the PR.
  2. The phase 1 job runs on every pull request and runs all 4 current test cases (interactive plus blocked stdin, each on Chromium and WebKit) with 0 skipped. In CI, a missing binary or esbuild fails the run rather than skipping.
  3. RED -> GREEN for phase 2. The Safari terminal smoke fails against a binary with the write_input fix reverted and passes with it. It runs in macos-arm-live-test.yml on pull requests and uploads a screenshot of the terminal.
  4. RED -> GREEN for phase 3. On current main, the Windows blocked-stdin test fails with slots leaked for about 30 s. After the kernal-api change and the version bump, it passes: all four slots come back within 5 s and no ConPTY child process survives. It runs in Windows CI.
  5. Phase 4 decisions are recorded in docs/interactive-terminal.md. Any code they call for has its own RED -> GREEN test.
  6. docs/interactive-terminal.md contains no /tmp/...nix path, /nix/store hash or personal worktree path, and its documented gate matches what CI runs.
  7. bash lint and bash test pass. No JSPI flags, WebAssembly.Suspending or WebAssembly.promising are introduced.

Decisions

Open questions

  • Windows ConPTY mechanism: a writer thread with a bounded hand-off is portable but costs a thread per session; overlapped I/O depends on how the input pipe was created in the ConPTY backend. Whoever implements phase 3 should pick one after reading platform_win.
  • Phase 1 image: running inside the Playwright container gives reproducible browser deps but needs the soldr toolchain set up inside it. --with-deps on the stock runner reuses the existing setup-soldr cache. The cache reuse probably matters more.
  • Safari smoke on x86 macOS: macos-x86-* only runs on main. Is ARM Safari on pull requests enough?

Related issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions