You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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?
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:
opens the terminal page served by the installed wheel's fastled;
waits for a shell prompt;
types printf 'SAFARI%s\n' 240 and asserts SAFARI240 in the xterm buffer;
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)
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.
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.
Run it in windows-x86-unit-test.yml or a Windows terminal job.
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).
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
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.
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.
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.
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.
Phase 4 decisions are recorded in docs/interactive-terminal.md. Any code they call for has its own RED -> GREEN test.
docs/interactive-terminal.md contains no /tmp/...nix path, /nix/store hash or personal worktree path, and its documented gate matches what CI runs.
bash lint and bash test pass. No JSPI flags, WebAssembly.Suspending or WebAssembly.promising are introduced.
One issue, five phases. They share a component and a test file, and phases 1 and 2 build on the same scenarios. Triage can split phase 3 (a kernal-api change plus a Windows test) if it grows.
Linux CI covers Chromium and WebKit; macOS CI covers real Safari. Playwright WebKit on Linux is cheap and catches engine-level breaks; only macOS proves Safari. Neither replaces the other.
Windows was confirmed by reading the code, not by running it. The code path is unambiguous (Unsupported → blocking write), but the phase 3 RED test must demonstrate it before any fix.
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?
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.
tests/frontend/test_terminal.pyruns in no workflow. None of the 30 files in.github/workflows/mention it or Playwright.bash testruns the Rust suite andtests/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.CLAUDE.mdmakes Safari a required production target.macos-arm-live-test.ymlalready enablessafaridriverand runsci/safari_smoke.py, but only against a compiled sketch, not/terminal/ws.platform_linux/terminal.rsandplatform_macos/terminal.rs. The default trait method inplatform/terminal.rsreturnsErrorKind::Unsupported.PtySession::write_availableinsrc/pty.rshandles that by falling back to the blockingwrite, and its own doc comment says "only the interruption guarantee is Unix-only". So on Windows,write_inputincrates/fastled-cli/src/terminal.rscan still park, and a disconnect cannot release its semaphore slot. The regression test isskipif(os.name == "nt")because it usesstty, so nothing catches this.docs/interactive-terminal.md(lines ~100-110) points at/tmp/fastled-terminal-shell.nix, which no longer exists, plus a pinned/nix/storePlaywright 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 tomain. It should:fastledbinary throughsoldr(reuse the_unit-test.ymlsetup and cache suffix);esbuild(via the FastLED toolchain or npm);playwright install --with-deps chromium webkitonubuntu-24.04, or run insidemcr.microsoft.com/playwright:v1.62.0-noble);FASTLED_TERMINAL_BINARY=... FASTLED_ESBUILD=... uv run --with playwright==<pin> pytest tests/frontend/test_terminal.py -v;The test must not skip silently in CI. When
CIis set, a missingFASTLED_TERMINAL_BINARYorFASTLED_ESBUILDmust fail the run. Otherwise a misconfigured job reports green.Phase 2: real Safari
Extend the macOS live test (
macos-arm-live-test.ymlwithci/safari_smoke.py, which already hassafaridriverenabled) with a terminal smoke that:fastled;printf 'SAFARI%s\n' 240and assertsSAFARI240in the xterm buffer;WebSocketfrom page JS.This is WebDriver, not Playwright, since Playwright cannot drive Safari.
Phase 3: Windows bounded write (fix upstream first, then here)
write_availablefor the Windows ConPTY backend, bounded bytimeout. 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.kernal-apiandkernal-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), notstty.windows-x86-unit-test.ymlor a Windows terminal job.Phase 4: decide the #256 leftovers
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.mdwith:playwright run-serveron 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 atplaywright==1.62.0, since client and server minor versions must match. For #258 that was a small pytest plugin patchingBrowserType.launch; a first-class env var such asFASTLED_PLAYWRIGHT_WEBKIT_ENDPOINThandled in the test file would be cleaner.Acceptance criteria
write_inputchange incrates/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 ontest_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.write_inputfix reverted and passes with it. It runs inmacos-arm-live-test.ymlon pull requests and uploads a screenshot of the terminal.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.docs/interactive-terminal.md. Any code they call for has its own RED -> GREEN test.docs/interactive-terminal.mdcontains no/tmp/...nixpath,/nix/storehash or personal worktree path, and its documented gate matches what CI runs.bash lintandbash testpass. No JSPI flags,WebAssembly.SuspendingorWebAssembly.promisingare introduced.Decisions
test_terminal.pyalready expects.Unsupported→ blockingwrite), but the phase 3 RED test must demonstrate it before any fix.Open questions
platform_win.--with-depson the stock runner reuses the existingsetup-soldrcache. The cache reuse probably matters more.macos-x86-*only runs onmain. Is ARM Safari on pull requests enough?Related issues