[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6QQE3185D3FVSW7VGZJCGGV":3,"skill-same:01M6QQE3185D3FVSW7VGZJCGGV":65},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":6,"ownerHtmlUrl":49,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":50,"install":51,"resources":64},"01M6QQE3185D3FVSW7VGZJCGGV","tui-tests",null,"Use when writing, fixing, or reviewing tests that render OpenTUI (testRender, createTestRenderer, createAppFixture) in packages\u002Ftui or packages\u002Fcli. Covers frame waits (waitForFrame, waitFor, waitForVisualIdle, flush), started vs non-started renderers, injected clocks, mock keyboard and mouse input, terminal sizes, cleanup and shared paths, and the 30s budget. Also use when a TUI test fails with \"Timed out waiting for frame predicate after N passes\", or when a tui test file is slow. For cli subprocess and service tests, use the unit-tests skill.","SimaDevelopment\u002Fopsiforce","SimaDevelopment","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F153810655?v=4","01M6QQ054MKBDSS43AND3FNHDK","Opsima","01M6QQ058VFT6EY7HCBVN49G8Z",42,"AGPL-3.0","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002F.opencode\u002Fskills\u002Ftui-tests\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"tui tests","tui","tests","input","frame","for","subprocess","unit-tests","render","unit","paths","opentui","writing","keyboard","shared","Budget","cleanup","packages","terminal","cli","2026-11-10 10:12:30",false,"---\nname: tui-tests\ndescription: Use when writing, fixing, or reviewing tests that render OpenTUI (testRender, createTestRenderer, createAppFixture) in packages\u002Ftui or packages\u002Fcli. Covers frame waits (waitForFrame, waitFor, waitForVisualIdle, flush), started vs non-started renderers, injected clocks, mock keyboard and mouse input, terminal sizes, cleanup and shared paths, and the 30s budget. Also use when a TUI test fails with \"Timed out waiting for frame predicate after N passes\", or when a tui test file is slow. For cli subprocess and service tests, use the unit-tests skill.\n---\n\n# TUI tests\n\npackages\u002Ftui runs about 170 test files in one `bun test` process. CI runs it next to up to two other suites on 4-vCPU runners (`bun turbo test --affected --concurrency=3` in `.github\u002Fworkflows\u002Ftest.yml`), where it has taken 136-146s on Linux and 147-171s on Windows, most of it idle on frame pacing, production timers and sleeps. Most flakes come from waiting on the wrong signal and then running out of frames on a busy runner.\n\nRead the root `AGENTS.md` Testing section (no `globalThis` mocks, run from the package directory) and its TUI Stories section. To drive the real TUI, use the `opencode-dev` skill. For CLI subprocesses, service deadlines, Effect `TestClock` and turbo test tasks, use the `unit-tests` skill.\n\n## Frame budgets are not time\n\n- `waitForFrame`, `waitFor` and `flush` give up after `maxPasses` (default 20). `waitForVisualIdle` gives up after `maxFrames` (default 20). One pass is one rendered frame (`DEFAULT_MAX_PASSES` in `@opentui\u002Fcore\u002Ftesting`). \"Timed out waiting for frame predicate after 20 passes\" comes from this budget, not from bun's timeout.\n- **Started renderer** (`setup.renderer.start()`, as `createAppFixture` does at `packages\u002Ftui\u002Ftest\u002Ffixture\u002Fapp.ts:28`): frames tick at OpenTUI's default 30 fps, so 20 passes are about 0.66s. Production renders at `targetFps: 60` (`packages\u002Ftui\u002Fsrc\u002Fapp.tsx:241`), so test frames are twice as slow as the real app.\n- **Not started** (the default for `testRender` and `createTestRenderer`): `waitFor` and `waitForFrame` fail within milliseconds when no render is scheduled, and the error shows `isRunning: false`. In the same state `flush()` and `waitForVisualIdle()` return immediately without error; they do not wait for fetches or timers. Await the signal, then `renderOnce()`.\n- Wait on frames only for something a frame produces. For anything else, wait on the signal itself:\n  - **Focus set on a timer.** `DialogSelect` focuses its filter with `setTimeout(..., 1)` (`packages\u002Ftui\u002Fsrc\u002Fui\u002Fdialog-select.tsx:702`), which takes about 15ms on Windows. `typeText` sends keys to whatever is focused, so early keys are dropped and Enter then picks the default option. Put the focus in the predicate, as both dialog waits in `packages\u002Ftui\u002Ftest\u002Fcommand-selection.test.tsx` do (`:68-70` for the model, `:75-80` for the variant): `setup.renderer.currentFocusedRenderable instanceof InputRenderable`. Before the variant wait had it, the test flaked in 5 CI runs in 20 hours, including a dev push, and a bigger budget did not help: it still failed with `maxPasses: 150`, because the keys were lost, not late. If a wait fails with the focus in the predicate, look for a state race, not the budget.\n  - **Markdown and code text.** `\u003Cmarkdown>` (`packages\u002Ftui\u002Fsrc\u002Froutes\u002Fsession\u002Fmessage-parts.tsx:182`) draws no text until the shared tree-sitter worker replies, while frames keep ticking, so a plain `waitForFrame` can spend all 20 passes on a loaded runner with the text still pending. Use `waitForHighlightedFrame(setup, predicate, options?)` from `packages\u002Ftui\u002Ftest\u002Ffixture\u002Fframe.ts`. When the predicate fails, it awaits every in-flight `CodeRenderable.highlightingDone` inside the frame predicate, and awaiting there does not use up passes. It accepts any setup with `renderer` and `waitForFrame`; app-lifecycle, session-group-navigation and session-home wait on transcript text this way. If the code under test accepts a client, inject `new MockTreeSitterClient({ autoResolveTimeout: 0 })` instead (`packages\u002Ftui\u002Ftest\u002Fmini\u002Fscrollback.surface.test.ts:87`).\n  - **Requests and other state.** Await the deferred response your fetch handler returns. With no signal to await, poll with a wall-clock deadline instead of raising `maxPasses`: the file-local `waitForFrame(setup, matches)` at `command-selection.test.tsx:117` checks `captureCharFrame()` every 10ms for up to 2s. `wait` in `packages\u002Ftui\u002Ftest\u002Fcli\u002Ftui\u002Fdata.test.tsx:37` and `waitForModel` in `packages\u002Ftui\u002Ftest\u002Ffixture\u002Flocal.tsx:109` do the same for state.\n- `waitForVisualIdle` returns on the first frame with `cellsUpdated === 0` by default. A frame waiting on a highlight is also quiet, so wait for the text before you measure layout or assert on it: `session-home.test.tsx:227` waits for \"History message 0001\" with `waitForHighlightedFrame` after `waitForVisualIdle()` instead of reading the frame directly.\n- Settle steps cost frames. Each `waitForVisualIdle({ quietFrames: 3 })` takes at least 3 frames, about 100ms at 30 fps. `session-group-navigation.test.tsx` has 14 of them. When you know what you are waiting for, wait for that frame instead.\n- Do not use `maxPasses` as a clock. Several waits do, and each assumes 33ms frames:\n  - `app-lifecycle.test.tsx:792`: 120 passes to cover the 1s reconnect delay after `setup.events.disconnect()` at `:784`;\n  - `app-lifecycle.test.tsx:1721-1723`: 200 passes per retry-countdown second;\n  - `component\u002Fdialog-shell-output.test.tsx:144`: 100 passes for a 1s poll, plus `Bun.sleep(1100)` at `:163` and `:182`;\n  - `session-panel.test.tsx:590`: 120 passes in `ready()` for the initial load.\n\n  With test frame rates scaled 4x, dialog-shell-output (3 tests) and session-activity-anchor (2 tests) failed. The same budgets are why tests cannot yet run at production fps. A larger budget is fine only for a long run of frame-driven work, such as scrolling 50 rows.\n\n## Pick the renderer mode and clock\n\n- **Component logic:** `testRender(() => \u003CX \u002F>, { width, height })` without `start()`. Change state, `await app.renderOnce()`, then assert on `captureCharFrame()`. One frame, deterministic.\n- **Time-driven renderables:** pass `clock: new ManualClock()`, call `renderer.pause()`, then advance the clock and `renderOnce()` (`fixture()` in `packages\u002Ftui\u002Ftest\u002Fcomponent\u002Fretry-provider.test.tsx:8`). `pause()` also stops `requestLive()` (spinners, animations) from starting the frame loop.\n- **Clock order:** inject a clock first (OpenTUI `ManualClock`; Effect `TestClock` for Effect code, see `unit-tests`). Use `jest.useFakeTimers()` only when nothing is injectable: it patches the global timers of a process that runs every tui file, so restore them with `jest.useRealTimers()` in `finally` (`packages\u002Ftui\u002Ftest\u002Futil\u002Fdelayed-presence.test.ts:24-26`).\n- **Whole app:** `createAppFixture` (`packages\u002Ftui\u002Ftest\u002Ffixture\u002Fapp.ts`) gives a started renderer, an in-process `Bun.serve` and `kittyKeyboard: true`. Call `await setup.ready` before the first wait. Passing `config` replaces the default `{ animations: false }`, so include `animations: false` yourself. The first use in a process pays `await import(\"..\u002F..\u002Fsrc\u002Fapp\")`: about 1s idle, 12-25s under heavy load. Use it only when the behavior spans routes or data.\n- **Server behavior:** use the existing `createFetch` and `createEventStream` fixture (`packages\u002Ftui\u002Ftest\u002Ffixture\u002Ftui-client.ts:15`, `:69`) rather than spawning a server. The persistent PTY, for example, is faked as `\u002Fapi\u002Fexperimental\u002Fpersistent-pty\u002F...` routes plus a `Bun.serve` websocket (`packages\u002Ftui\u002Ftest\u002Fsession-wheel-focus.test.tsx:52`); real PTY behavior is tested in `packages\u002Fserver\u002Ftest\u002Fpersistent-pty.test.ts`. Every fixture server listens on `port: 0` (`fixture\u002Fapp.ts:32`) and is reached through `server.url`; never hard-code a port. Windows hands out ephemeral ports from 49152 up, so a fixed port in that range can already be taken by another connection.\n\n## Keyboard and mouse\n\n- Use `setup.mockInput`: `pressKey`, `typeText`, `pressEnter`, `pressEscape`, `pressArrow`, `pasteBracketedText`.\n- Create renderers with `kittyKeyboard: true`. Without it, `mockInput` silently sends something else (OpenTUI 0.5.17):\n\n  | Input                                        | Without kitty mode                        | With kitty mode |\n  | -------------------------------------------- | ----------------------------------------- | --------------- |\n  | `pressKey(\"u\", { ctrl: true, shift: true })` | `ctrl+u`                                  | `ctrl+shift+u`  |\n  | `pressKey(\"2\", { ctrl: true })`              | `2`                                       | `ctrl+2`        |\n  | `pressEnter({ shift: true })`                | `return`                                  | `shift+return`  |\n  | `pressEscape()` then `pressKey(\"a\")`         | `meta+a` (a lone Escape is held for 20ms) | `escape`, `a`   |\n\n- A second click on the same renderable within 500ms (renderer clock) and within 1 cell becomes a word or line selection. Do not sleep past that window: `Bun.sleep(550)` at `packages\u002Ftui\u002Ftest\u002Fsession-activity-anchor.test.tsx:120` costs 4.4s per file. Call `setup.renderer.clearSelection()` first, which resets the click history, or click at least 2 cells away. In a `ManualClock` renderer driven by `renderOnce()`, `clock.advance(501)` also works.\n\n## Terminal sizes\n\n- Always pass `width` and `height`. Otherwise the renderer uses `process.stdout.columns`: your terminal locally, 80 in CI.\n- Cover layout at a narrow and a wide width, for example `test.each([100, 44])` (`packages\u002Ftui\u002Ftest\u002Fapp-lifecycle.test.tsx:13`). After `setup.resize(w, h)`, `await setup.renderOnce()` or `flush()` before asserting. Run any real-time sequence at one width only.\n- Measure work by counting, not timing: mounted rows (`packages\u002Ftui\u002Ftest\u002Fsession-mount-budget.test.tsx`), rendered diff lines (`packages\u002Ftui\u002Ftest\u002Fcli\u002Ftui\u002Fdiff-viewer.test.tsx:2198`). `performance.now()` thresholds fail on slow Windows runners.\n\n## Timers, cleanup and shared state\n\n- A TUI test that takes over a second is usually waiting on a production constant; make it injectable instead of waiting it out. Two such constants: the shell output poll `setTimeout(poll, more ? 0 : 1_000)` (`packages\u002Ftui\u002Fsrc\u002Fcomponent\u002Fdialog-shell-output.tsx:73`; 6 tests, 18.8s, 95% idle) and `reconnectDelay = 1_000` (`packages\u002Fclient\u002Fsrc\u002Fsolid\u002Fconnection.ts:33`; about 1s per reconnect test). connection.ts already takes `idleTimeout` and `flushInterval` options, so add a sibling.\n- Clean up with `await using` or `finally`: destroy the renderer and stop the fixture server. Cancel timers first, or they throw \"TextBuffer is destroyed\" into the next test.\n- Remove the tmpdir last. Destroy the renderer and await the app task first, as `createAppFixture`'s `[Symbol.asyncDispose]` does (`fixture\u002Fapp.ts:63`). Removing it under a pending write logs \"Failed to persist\" and flock \"lock is compromised\".\n- `TestTuiContexts` (`packages\u002Ftui\u002Ftest\u002Ffixture\u002Ftui-environment.tsx:32-35`) defaults `home` and `state` to fixed `\u002Ftmp\u002Fopencode\u002F...` paths. When the tree mounts a provider that writes under `paths.state` (`StorageProvider` in `src\u002Fcontext\u002Fstorage.tsx:145`, `LocalProvider`, or the prompt history, frecency and stash providers), pass `paths={{ state, home }}` built from `tmpdir()` (`test\u002Ffixture\u002Ffixture.ts:8`). Process-shared paths are what keep tui from parallel workers: `--parallel=2 --no-isolate` halved local wall time (162.9s to 80.7s). Isolated mode re-imports `src\u002Fapp` in each of the 18 test files that load it, at 6x the CPU.\n- Bun does not cancel a timed-out test body. It keeps running and can fail the next test or rerun as \"Unhandled error between tests\". Triage the first failure in a file; the ones after it may be leftovers.\n- Import another package's test helpers by relative path only from a package in tui's workspace dependency closure. `@opencode\u002Ftui#test` depends on `transit` in `turbo.json`, which folds tui's whole workspace dependency closure into the test hash, so `data.test.tsx:7` reaching `..\u002F..\u002F..\u002F..\u002Fcore\u002Ftest` is covered because tui depends on `@opencode\u002Fcore`. A package outside that closure is not hashed, and a change there can replay a stale pass (see `unit-tests`, Turbo test tasks).\n\n## Timeouts and failures\n\n- The tui and cli test scripts run `bun test --timeout 30000`; Bun's default is 5s. In tui, that budget exists for the cold `src\u002Fapp` import, not for slow waits.\n- A frame-budget failure is not a timeout problem. Raising the test timeout does nothing for it, and raising `maxPasses` only hides the wrong wait.\n- A per-test timeout below 30s, like the 15000 at `session-group-navigation.test.tsx:249`, must still cover the cold import when the file runs alone. New per-test timeouts name their work in a comment (see `unit-tests`, Know the budget).\n- A frame-budget failure that reproduces on every run of one branch is a regression in that change, not a flake. dialog-shell-output failed \"after 100 passes\" in 21 of 29 tui runs on one PR stack and in 0 of about 102 elsewhere. Do not answer it with a bigger budget.\n- Do not add `skipIf(process.platform === \"win32\")` to hide a timing flake; fix the wait. Windows timers have about 15ms resolution, so a 1ms `setTimeout` race that never shows on macOS fails there.\n\n## Before you push\n\n- From `packages\u002Ftui`: `bun test --timeout 30000 test\u002F\u003Cfile>`. This matches CI's budget and prints per-test milliseconds, which the package script's `--only-failures` hides.\n- `bun test --timeout 30000 test\u002F\u003Cfile> --rerun-each 10`: all 10 must pass. The first iteration pays the cold import, so a timeout on iteration 1 under heavy load is that import, not your wait. Repeat while one other suite runs (for example `bun run test` in `packages\u002Fcore`), not on an overloaded machine. Before its focus wait, the command-selection race failed 5 of 8 local runs under load.\n- Force the race you suspect: temporarily slow the timer or reply (for example the focus `setTimeout` to 150ms). The test must still pass. Revert afterwards.\n- Negative control: make the predicate impossible. The test must fail fast with a frame dump, not hang until the timeout.\n- Windows-sensitive behavior to check for: timers under 16ms, renaming a file another process holds, process kill, and paths shown in frames. On pushes and pull requests the unit job runs `--affected`, so `@opencode\u002Ftui:test` is scheduled only when tui, a workspace package it depends on, or a `globalDependencies` file in `turbo.json` changed. In the `unit (windows)` log, confirm the lines prefixed `@opencode\u002Ftui:test:` are there with `cache miss, executing` and a pass, and compare its `Ran N tests across M files. [Xs]` line with a recent run that included tui. Per-file timing is only available locally.\n","# TUI tests\n\npackages\u002Ftui runs about 170 test files in one `bun test` process. CI runs it next to up to two other suites on 4-vCPU runners (`bun turbo test --affected --concurrency=3` in `.github\u002Fworkflows\u002Ftest.yml`), where it has taken 136-146s on Linux and 147-171s on Windows, most of it idle on frame pacing, production timers and sleeps. Most flakes come from waiting on the wrong signal and then running out of frames on a busy runner.\n\nRead the root `AGENTS.md` Testing section (no `globalThis` mocks, run from the package directory) and its TUI Stories section. To drive the real TUI, use the `opencode-dev` skill. For CLI subprocesses, service deadlines, Effect `TestClock` and turbo test tasks, use the `unit-tests` skill.\n\n## Frame budgets are not time\n\n- `waitForFrame`, `waitFor` and `flush` give up after `maxPasses` (default 20). `waitForVisualIdle` gives up after `maxFrames` (default 20). One pass is one rendered frame (`DEFAULT_MAX_PASSES` in `@opentui\u002Fcore\u002Ftesting`). \"Timed out waiting for frame predicate after 20 passes\" comes from this budget, not from bun's timeout.\n- **Started renderer** (`setup.renderer.start()`, as `createAppFixture` does at `packages\u002Ftui\u002Ftest\u002Ffixture\u002Fapp.ts:28`): frames tick at OpenTUI's default 30 fps, so 20 passes are about 0.66s. Production renders at `targetFps: 60` (`packages\u002Ftui\u002Fsrc\u002Fapp.tsx:241`), so test frames are twice as slow as the real app.\n- **Not started** (the default for `testRender` and `createTestRenderer`): `waitFor` and `waitForFrame` fail within milliseconds when no render is scheduled, and the error shows `isRunning: false`. In the same state `flush()` and `waitForVisualIdle()` return immediately without error; they do not wait for fetches or timers. Await the signal, then `renderOnce()`.\n- Wait on frames only for something a frame produces. For anything else, wait on the signal itself:\n  - **Focus set on a timer.** `DialogSelect` focuses its filter with `setTimeout(..., 1)` (`packages\u002Ftui\u002Fsrc\u002Fui\u002Fdialog-select.tsx:702`), which takes about 15ms on Windows. `typeText` sends keys to whatever is focused, so early keys are dropped and Enter then picks the default option. Put the focus in the predicate, as both dialog waits in `packages\u002Ftui\u002Ftest\u002Fcommand-selection.test.tsx` do (`:68-70` for the model, `:75-80` for the variant): `setup.renderer.currentFocusedRenderable instanceof InputRenderable`. Before the variant wait had it, the test flaked in 5 CI runs in 20 hours, including a dev push, and a bigger budget did not help: it still failed with `maxPasses: 150`, because the keys were lost, not late. If a wait fails with the focus in the predicate, look for a state race, not the budget.\n  - **Markdown and code text.** `\u003Cmarkdown>` (`packages\u002Ftui\u002Fsrc\u002Froutes\u002Fsession\u002Fmessage-parts.tsx:182`) draws no text until the shared tree-sitter worker replies, while frames keep ticking, so a plain `waitForFrame` can spend all 20 passes on a loaded runner with the text still pending. Use `waitForHighlightedFrame(setup, predicate, options?)` from `packages\u002Ftui\u002Ftest\u002Ffixture\u002Fframe.ts`. When the predicate fails, it awaits every in-flight `CodeRenderable.highlightingDone` inside the frame predicate, and awaiting there does not use up passes. It accepts any setup with `renderer` and `waitForFrame`; app-lifecycle, session-group-navigation and session-home wait on transcript text this way. If the code under test accepts a client, inject `new MockTreeSitterClient({ autoResolveTimeout: 0 })` instead (`packages\u002Ftui\u002Ftest\u002Fmini\u002Fscrollback.surface.test.ts:87`).\n  - **Requests and other state.** Await the deferred response your fetch handler returns. With no signal to await, poll with a wall-clock deadline instead of raising `maxPasses`: the file-local `waitForFrame(setup, matches)` at `command-selection.test.tsx:117` checks `captureCharFrame()` every 10ms for up to 2s. `wait` in `packages\u002Ftui\u002Ftest\u002Fcli\u002Ftui\u002Fdata.test.tsx:37` and `waitForModel` in `packages\u002Ftui\u002Ftest\u002Ffixture\u002Flocal.tsx:109` do the same for state.\n- `waitForVisualIdle` returns on the first frame with `cellsUpdated === 0` by default. A frame waiting on a highlight is also quiet, so wait for the text before you measure layout or assert on it: `session-home.test.tsx:227` waits for \"History message 0001\" with `waitForHighlightedFrame` after `waitForVisualIdle()` instead of reading the frame directly.\n- Settle steps cost frames. Each `waitForVisualIdle({ quietFrames: 3 })` takes at least 3 frames, about 100ms at 30 fps. `session-group-navigation.test.tsx` has 14 of them. When you know what you are waiting for, wait for that frame instead.\n- Do not use `maxPasses` as a clock. Several waits do, and each assumes 33ms frames:\n  - `app-lifecycle.test.tsx:792`: 120 passes to cover the 1s reconnect delay after `setup.events.disconnect()` at `:784`;\n  - `app-lifecycle.test.tsx:1721-1723`: 200 passes per retry-countdown second;\n  - `component\u002Fdialog-shell-output.test.tsx:144`: 100 passes for a 1s poll, plus `Bun.sleep(1100)` at `:163` and `:182`;\n  - `session-panel.test.tsx:590`: 120 passes in `ready()` for the initial load.\n\n  With test frame rates scaled 4x, dialog-shell-output (3 tests) and session-activity-anchor (2 tests) failed. The same budgets are why tests cannot yet run at production fps. A larger budget is fine only for a long run of frame-driven work, such as scrolling 50 rows.\n\n## Pick the renderer mode and clock\n\n- **Component logic:** `testRender(() => \u003CX \u002F>, { width, height })` without `start()`. Change state, `await app.renderOnce()`, then assert on `captureCharFrame()`. One frame, deterministic.\n- **Time-driven renderables:** pass `clock: new ManualClock()`, call `renderer.pause()`, then advance the clock and `renderOnce()` (`fixture()` in `packages\u002Ftui\u002Ftest\u002Fcomponent\u002Fretry-provider.test.tsx:8`). `pause()` also stops `requestLive()` (spinners, animations) from starting the frame loop.\n- **Clock order:** inject a clock first (OpenTUI `ManualClock`; Effect `TestClock` for Effect code, see `unit-tests`). Use `jest.useFakeTimers()` only when nothing is injectable: it patches the global timers of a process that runs every tui file, so restore them with `jest.useRealTimers()` in `finally` (`packages\u002Ftui\u002Ftest\u002Futil\u002Fdelayed-presence.test.ts:24-26`).\n- **Whole app:** `createAppFixture` (`packages\u002Ftui\u002Ftest\u002Ffixture\u002Fapp.ts`) gives a started renderer, an in-process `Bun.serve` and `kittyKeyboard: true`. Call `await setup.ready` before the first wait. Passing `config` replaces the default `{ animations: false }`, so include `animations: false` yourself. The first use in a process pays `await import(\"..\u002F..\u002Fsrc\u002Fapp\")`: about 1s idle, 12-25s under heavy load. Use it only when the behavior spans routes or data.\n- **Server behavior:** use the existing `createFetch` and `createEventStream` fixture (`packages\u002Ftui\u002Ftest\u002Ffixture\u002Ftui-client.ts:15`, `:69`) rather than spawning a server. The persistent PTY, for example, is faked as `\u002Fapi\u002Fexperimental\u002Fpersistent-pty\u002F...` routes plus a `Bun.serve` websocket (`packages\u002Ftui\u002Ftest\u002Fsession-wheel-focus.test.tsx:52`); real PTY behavior is tested in `packages\u002Fserver\u002Ftest\u002Fpersistent-pty.test.ts`. Every fixture server listens on `port: 0` (`fixture\u002Fapp.ts:32`) and is reached through `server.url`; never hard-code a port. Windows hands out ephemeral ports from 49152 up, so a fixed port in that range can already be taken by another connection.\n\n## Keyboard and mouse\n\n- Use `setup.mockInput`: `pressKey`, `typeText`, `pressEnter`, `pressEscape`, `pressArrow`, `pasteBracketedText`.\n- Create renderers with `kittyKeyboard: true`. Without it, `mockInput` silently sends something else (OpenTUI 0.5.17):\n\n  | Input                                        | Without kitty mode                        | With kitty mode |\n  | -------------------------------------------- | ----------------------------------------- | --------------- |\n  | `pressKey(\"u\", { ctrl: true, shift: true })` | `ctrl+u`                                  | `ctrl+shift+u`  |\n  | `pressKey(\"2\", { ctrl: true })`              | `2`                                       | `ctrl+2`        |\n  | `pressEnter({ shift: true })`                | `return`                                  | `shift+return`  |\n  | `pressEscape()` then `pressKey(\"a\")`         | `meta+a` (a lone Escape is held for 20ms) | `escape`, `a`   |\n\n- A second click on the same renderable within 500ms (renderer clock) and within 1 cell becomes a word or line selection. Do not sleep past that window: `Bun.sleep(550)` at `packages\u002Ftui\u002Ftest\u002Fsession-activity-anchor.test.tsx:120` costs 4.4s per file. Call `setup.renderer.clearSelection()` first, which resets the click history, or click at least 2 cells away. In a `ManualClock` renderer driven by `renderOnce()`, `clock.advance(501)` also works.\n\n## Terminal sizes\n\n- Always pass `width` and `height`. Otherwise the renderer uses `process.stdout.columns`: your terminal locally, 80 in CI.\n- Cover layout at a narrow and a wide width, for example `test.each([100, 44])` (`packages\u002Ftui\u002Ftest\u002Fapp-lifecycle.test.tsx:13`). After `setup.resize(w, h)`, `await setup.renderOnce()` or `flush()` before asserting. Run any real-time sequence at one width only.\n- Measure work by counting, not timing: mounted rows (`packages\u002Ftui\u002Ftest\u002Fsession-mount-budget.test.tsx`), rendered diff lines (`packages\u002Ftui\u002Ftest\u002Fcli\u002Ftui\u002Fdiff-viewer.test.tsx:2198`). `performance.now()` thresholds fail on slow Windows runners.\n\n## Timers, cleanup and shared state\n\n- A TUI test that takes over a second is usually waiting on a production constant; make it injectable instead of waiting it out. Two such constants: the shell output poll `setTimeout(poll, more ? 0 : 1_000)` (`packages\u002Ftui\u002Fsrc\u002Fcomponent\u002Fdialog-shell-output.tsx:73`; 6 tests, 18.8s, 95% idle) and `reconnectDelay = 1_000` (`packages\u002Fclient\u002Fsrc\u002Fsolid\u002Fconnection.ts:33`; about 1s per reconnect test). connection.ts already takes `idleTimeout` and `flushInterval` options, so add a sibling.\n- Clean up with `await using` or `finally`: destroy the renderer and stop the fixture server. Cancel timers first, or they throw \"TextBuffer is destroyed\" into the next test.\n- Remove the tmpdir last. Destroy the renderer and await the app task first, as `createAppFixture`'s `[Symbol.asyncDispose]` does (`fixture\u002Fapp.ts:63`). Removing it under a pending write logs \"Failed to persist\" and flock \"lock is compromised\".\n- `TestTuiContexts` (`packages\u002Ftui\u002Ftest\u002Ffixture\u002Ftui-environment.tsx:32-35`) defaults `home` and `state` to fixed `\u002Ftmp\u002Fopencode\u002F...` paths. When the tree mounts a provider that writes under `paths.state` (`StorageProvider` in `src\u002Fcontext\u002Fstorage.tsx:145`, `LocalProvider`, or the prompt history, frecency and stash providers), pass `paths={{ state, home }}` built from `tmpdir()` (`test\u002Ffixture\u002Ffixture.ts:8`). Process-shared paths are what keep tui from parallel workers: `--parallel=2 --no-isolate` halved local wall time (162.9s to 80.7s). Isolated mode re-imports `src\u002Fapp` in each of the 18 test files that load it, at 6x the CPU.\n- Bun does not cancel a timed-out test body. It keeps running and can fail the next test or rerun as \"Unhandled error between tests\". Triage the first failure in a file; the ones after it may be leftovers.\n- Import another package's test helpers by relative path only from a package in tui's workspace dependency closure. `@opencode\u002Ftui#test` depends on `transit` in `turbo.json`, which folds tui's whole workspace dependency closure into the test hash, so `data.test.tsx:7` reaching `..\u002F..\u002F..\u002F..\u002Fcore\u002Ftest` is covered because tui depends on `@opencode\u002Fcore`. A package outside that closure is not hashed, and a change there can replay a stale pass (see `unit-tests`, Turbo test tasks).\n\n## Timeouts and failures\n\n- The tui and cli test scripts run `bun test --timeout 30000`; Bun's default is 5s. In tui, that budget exists for the cold `src\u002Fapp` import, not for slow waits.\n- A frame-budget failure is not a timeout problem. Raising the test timeout does nothing for it, and raising `maxPasses` only hides the wrong wait.\n- A per-test timeout below 30s, like the 15000 at `session-group-navigation.test.tsx:249`, must still cover the cold import when the file runs alone. New per-test timeouts name their work in a comment (see `unit-tests`, Know the budget).\n- A frame-budget failure that reproduces on every run of one branch is a regression in that change, not a flake. dialog-shell-output failed \"after 100 passes\" in 21 of 29 tui runs on one PR stack and in 0 of about 102 elsewhere. Do not answer it with a bigger budget.\n- Do not add `skipIf(process.platform === \"win32\")` to hide a timing flake; fix the wait. Windows timers have about 15ms resolution, so a 1ms `setTimeout` race that never shows on macOS fails there.\n\n## Before you push\n\n- From `packages\u002Ftui`: `bun test --timeout 30000 test\u002F\u003Cfile>`. This matches CI's budget and prints per-test milliseconds, which the package script's `--only-failures` hides.\n- `bun test --timeout 30000 test\u002F\u003Cfile> --rerun-each 10`: all 10 must pass. The first iteration pays the cold import, so a timeout on iteration 1 under heavy load is that import, not your wait. Repeat while one other suite runs (for example `bun run test` in `packages\u002Fcore`), not on an overloaded machine. Before its focus wait, the command-selection race failed 5 of 8 local runs under load.\n- Force the race you suspect: temporarily slow the timer or reply (for example the focus `setTimeout` to 150ms). The test must still pass. Revert afterwards.\n- Negative control: make the predicate impossible. The test must fail fast with a frame dump, not hang until the timeout.\n- Windows-sensitive behavior to check for: timers under 16ms, renaming a file another process holds, process kill, and paths shown in frames. On pushes and pull requests the unit job runs `--affected`, so `@opencode\u002Ftui:test` is scheduled only when tui, a workspace package it depends on, or a `globalDependencies` file in `turbo.json` changed. In the `unit (windows)` log, confirm the lines prefixed `@opencode\u002Ftui:test:` are there with `cache miss, executing` and a pass, and compare its `Ran N tests across M files. [Xs]` line with a recent run that included tui. Per-file timing is only available locally.\n",{"name":5,"description":7},"main","c54d8e35f50ac35b3b2c7ba8d4fca2205a740cb9","frontend\u002Fopencode\u002F.opencode\u002Fskills\u002Ftui-tests",9,"https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce","https:\u002F\u002Fgithub.com\u002FSimaDevelopment","2026-11-05 06:13:10",{"source":8,"skillName":5,"commands":52,"prompt":62,"zipAvailable":63},[53,56,59],{"tool":54,"command":55},"npx","npx skills add SimaDevelopment\u002Fopsiforce --skill tui-tests",{"tool":57,"command":58},"bunx","bunx skills add SimaDevelopment\u002Fopsiforce --skill tui-tests",{"tool":60,"command":61},"pnpm","pnpm dlx skills add SimaDevelopment\u002Fopsiforce --skill tui-tests","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002F.opencode\u002Fskills\u002Ftui-tests\u002FSKILL.md",true,[],[66,106,125,146,156,182],{"skillNo":67,"name":68,"nameZh":6,"description":69,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":85,"publishedAt":39},"01M6QQKHZFGFB6A111RW2A9WC0","figma-use","**MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug failures. Trigger whenever the user wants to perform a write action or a unique read action that requires JavaScript execution in the Figma file context — e.g. create\u002Fedit\u002Fdelete nodes, set up variables or tokens, build components and variants, modify auto-layout or fills, bind variables to properties, or inspect file structure programmatically.","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002Fpackages\u002Fplugins\u002Ffigma\u002Fskills\u002Ffigma-use\u002FSKILL.md",[72,77,82],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",{"categoryNo":78,"categoryName":79,"taxonomyType":80,"taxonomyTypeDesc":81},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",{"categoryNo":83,"categoryName":84,"taxonomyType":80,"taxonomyTypeDesc":81},"01JBSEEDCAT000000000000032","设计师",[86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105],"figma use","figma","use","components","create","delete","tool","read","write","edit","context","auto","structure","tokens","bind","Variables","you","inspect","variants","invoke",{"skillNo":107,"name":108,"nameZh":6,"description":109,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":110,"categories":111,"keywords":117,"publishedAt":39},"01M6QQK1AMA90K6DFAVANYQ9X3","figma-use-slides","This skill helps agents use Figma's use_figma MCP tool in the Slides context. Can be used alongside figma-use which has foundational context for using the use_figma tool.","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002Fpackages\u002Fplugins\u002Ffigma\u002Fskills\u002Ffigma-use-slides\u002FSKILL.md",[112,115,116],{"categoryNo":113,"categoryName":114,"taxonomyType":75,"taxonomyTypeDesc":76},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},{"categoryNo":83,"categoryName":84,"taxonomyType":80,"taxonomyTypeDesc":81},[86,87,118,119,88,120,92,96,121,122,123,24,124],"figma use slides","slides","MCP","false","which","can","figma mcp",{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":133,"publishedAt":39},"01M6QQJTR9QYK297WQ1X792D63","figma-use-motion","Motion \u002F animation context for the `use_figma` MCP tool — animating Figma nodes via manual keyframes, animation styles, easing, and timeline duration. Load alongside figma-use whenever a task involves adding, editing, or inspecting animation on a node.","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002Fpackages\u002Fplugins\u002Ffigma\u002Fskills\u002Ffigma-use-motion\u002FSKILL.md",[131,132],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},{"categoryNo":83,"categoryName":84,"taxonomyType":80,"taxonomyTypeDesc":81},[86,87,134,88,135,120,136,92,137,138,96,139,140,141,121,142,143,144,145,24],"figma use motion","motion","Animation","node","editing","Nodes","task","keyframes","via","styles","manual","load",{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":154,"publishedAt":39},"01M6QQJNZ067ZRXZ6A0QB9M6SB","figma-use-figjam","This skill helps agents use Figma's use_figma MCP tool in the FigJam context. Can be used alongside figma-use which has foundational context for using the use_figma tool.","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002Fpackages\u002Fplugins\u002Ffigma\u002Fskills\u002Ffigma-use-figjam\u002FSKILL.md",[152,153],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},{"categoryNo":83,"categoryName":84,"taxonomyType":80,"taxonomyTypeDesc":81},[86,87,155,88,120,92,96,121,122,123,24,124],"figma use figjam",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":163,"publishedAt":39},"01M6QQJ7NEHXPE33VRVHYYM7YE","figma-swiftui","SwiftUI ↔ Figma translation. Use whenever the user mentions Swift, SwiftUI, iOS, iPhone, or iPad — in EITHER direction — translating a Figma design into SwiftUI (design → code), or pushing SwiftUI views \u002F screens \u002F tokens back into a Figma file (code → design). Triggers on phrases like 'implement this Figma design in SwiftUI', 'build this screen in Swift', 'push this SwiftUI view to Figma', 'mirror my Swift code in a Figma file', or whenever a Figma URL appears alongside `.swift` files \u002F an `.xcodeproj`. Routes to a direction-specific reference doc; loads alongside `figma-use` for the code → design path.","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002Fpackages\u002Fplugins\u002Ffigma\u002Fskills\u002Ffigma-swiftui\u002FSKILL.md",[162],{"categoryNo":83,"categoryName":84,"taxonomyType":80,"taxonomyTypeDesc":81},[87,164,165,166,167,168,169,170,171,172,173,174,175,176,121,177,178,179,180,181],"figma swiftui","swiftui","design","swift","code","push","doc","files","translation","back","either","iphone","ipad","screen","direction","URL","my","path",{"skillNo":183,"name":184,"nameZh":6,"description":185,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":186,"categories":187,"keywords":189,"publishedAt":39},"01M6QQJ3WZ3M90BZQ5TD305PKN","figma-shaders","**MANDATORY prerequisite** — load this skill before calling `create_shader` or `update_shader`. Use when the user asks to create, author, change, fix, or iterate on a shader effect, shader fill, custom effect, custom fill, or procedural shader in Figma.","https:\u002F\u002Fgithub.com\u002FSimaDevelopment\u002Fopsiforce\u002Fblob\u002Fmain\u002Ffrontend\u002Fopencode\u002Fpackages\u002Fplugins\u002Ffigma\u002Fskills\u002Ffigma-shaders\u002FSKILL.md",[188],{"categoryNo":83,"categoryName":84,"taxonomyType":80,"taxonomyTypeDesc":81},[190,87,191,192,90,193,194,195,196,88,197,198,121,199,145,200,201,202,203],"shaders","figma shaders","change","update","effect","user","fix","shader","calling","procedural","fill","iterate","custom","author"]