Personal outliner built with Rust.
4

Configure Feed

Select the types of activity you want to include in your feed.

propose add-dev-automation: gpui ui-test-harness, devtools automation server, fixture graphs

graham.systems (Jul 14, 2026, 10:52 AM -0700) 1e5631f6 f262b286

+325
+2
openspec/changes/add-dev-automation/.openspec.yaml
··· 1 + schema: spec-driven 2 + created: 2026-07-14
+96
openspec/changes/add-dev-automation/design.md
··· 1 + # Design: add-dev-automation 2 + 3 + ## Context 4 + 5 + Trawler's UI is GPUI (Direct3D/Metal/Vulkan-rendered, no DOM or accessibility tree), so nothing outside the process can introspect or drive it. All existing tests live in `trawler-core`; the `trawler` UI crate (~2,800-line `main.rs` plus `editor.rs`) has zero automated coverage, and visual work is verified entirely by hand. The immediate roadmap is styling/layout, and contributors on non-Windows platforms are expected soon, so the tooling must be Rust-native and platform-uniform. 6 + 7 + API surface verified against the pinned `gpui 0.2.2` crate source (not from memory): 8 + 9 + - `Window::dispatch_keystroke(keystroke, cx) -> bool` (`src/window.rs:3540`) — dispatches through the real keymap on a production window. 10 + - `Keystroke::parse("ctrl-k")` (`src/platform/keystroke.rs:120`) — gpui's own keystroke syntax, reusable as the wire format. 11 + - `Window::bounds()` / `window_bounds()` — window geometry, queryable at runtime. 12 + - `test-support` feature: `TestAppContext`/`VisualTestContext` with `simulate_keystrokes`, `simulate_input`, `simulate_click`, `simulate_event` (`src/app/test_context.rs`), backed by a pure-Rust fake platform — headless and identical on all OSes. 13 + - gpui has **no** screenshot/offscreen-render API (zero hits in crate source); pixel capture must come from OS-level window capture. 14 + - gpui depends on `futures` and `smol`; a `futures::channel::mpsc` sender is `Send`, so a background thread can feed a foreground task spawned with `cx.spawn` — the standard pattern for marshaling external events onto the GPUI main thread. 15 + 16 + ## Goals / Non-Goals 17 + 18 + **Goals:** 19 + 20 + - An agent or contributor on any platform can, with one cargo command, launch trawler in a mode where a local client can: inject keystrokes/text, read structured UI state, query window bounds, and save a screenshot PNG. 21 + - Headless, deterministic gpui tests of the real `TrawlerApp` view covering the documented keyboard editor behaviors, running in `cargo test --workspace` on all platforms and CI. 22 + - Reproducible content: tests and interactive dev sessions seed the same deterministic fixture graph, so state assertions and screenshots are comparable across machines and sessions. 23 + - Zero footprint in release builds and zero change to user-facing behavior. 24 + 25 + **Non-Goals:** 26 + 27 + - Reliable screenshots on Linux Wayland (compositor-dependent; documented best-effort — all non-screenshot commands still work there). 28 + - Pixel-diff/golden-image testing infrastructure (screenshots are for human/agent inspection, not automated comparison — can be layered on later). 29 + - Remote or multi-client automation, authentication, or any non-localhost transport. 30 + - Mouse-coordinate automation as a primary mechanism (trawler is keyboard-first; `dump` + keystrokes cover the flows; bounds are exposed so a client *can* compute coordinates if ever needed). 31 + - Driving the app through OS input APIs (SendInput/CGEvent/XTEST) — explicitly rejected, see D2. 32 + 33 + ## Decisions 34 + 35 + ### D1 — Three capabilities, one change 36 + 37 + `ui-test-harness` (test-only), `dev-automation-server` (runtime, feature-gated), and `fixture-graphs` (shared infrastructure) ship together because the fixture builder is load-bearing for both consumers; splitting would leave it awkwardly owned by whichever change landed first. They are implementable sequentially (fixtures → harness → server). 38 + 39 + ### D2 — Inject input through GPUI dispatch, not OS input APIs 40 + 41 + The dev server calls `Window::dispatch_keystroke` (and the text-input path for `type`) on the main thread. Alternatives considered: OS-level injection (`SendInput`/enigo) requires window focus, steals the user's keyboard, needs per-OS code, and is timing-flaky. GPUI dispatch is focus-independent, synchronous (the reply is sent after the action completed — no sleeps), byte-identical across platforms, and exercises the real keymap, so a passing interaction implies the user-visible binding works. 42 + 43 + ### D3 — Transport: localhost TCP, JSON lines 44 + 45 + One request per line, one JSON response per line, bound to `127.0.0.1` on a port chosen by the OS (port 0) and printed to stdout plus written to `<graph-dir>/devtools.port` for discovery. Alternatives: named pipes/unix domain sockets are per-OS (the exact portability trap this change exists to avoid); HTTP adds a dependency and buys nothing for a line-oriented request/response protocol driven by `curl`-less agents (any language can speak "write a line, read a line" over TCP). 46 + 47 + Command set (v1): 48 + 49 + | Request | Response | 50 + |---|---| 51 + | `{"cmd":"keys","keys":"ctrl-k"}` | `{"ok":true}` after dispatch; keystroke syntax is `Keystroke::parse` | 52 + | `{"cmd":"type","text":"hello"}` | `{"ok":true}` after text insertion | 53 + | `{"cmd":"dump"}` | structured UI state (see D5) | 54 + | `{"cmd":"bounds"}` | `{"x":..,"y":..,"width":..,"height":..,"scale":..}` | 55 + | `{"cmd":"screenshot","path":"..."}` | `{"ok":true,"path":"..."}` after PNG written | 56 + 57 + Unknown commands and failures return `{"ok":false,"error":"..."}`; the server never panics the app. 58 + 59 + ### D4 — Threading: acceptor thread → mpsc → foreground task 60 + 61 + The listener runs on a plain `std::thread` doing blocking line reads. Each parsed command plus a oneshot reply sender goes into a `futures::channel::mpsc::UnboundedSender` (Send, wakes the main loop correctly). At startup the app spawns one foreground task (`cx.spawn`) that loops `rx.next().await`, executes the command against the window/entities via the async handle, and replies. Alternative — running an async runtime inside the app — rejected: gpui's executors already provide the async side, and one blocking thread per connection (single client expected) is the simplest correct thing. 62 + 63 + ### D5 — `dump` returns semantic state, not a render tree 64 + 65 + The dump serializes what a test or agent actually asserts on: current view (journal date / page name / search), the visible outline as a nested block list (id, text, depth, collapsed), focused block id, cursor offset and selection, any open popup (completion candidates, quick-open items, calendar month), navigation stack summary, and window bounds. It is built from `TrawlerApp`/`BlockEditor` entity state (`main.rs:173`), not by walking GPUI elements — element-tree introspection is brittle across refactors and gpui upgrades, and the entity state is the source of truth the UI renders from. The schema is versioned with a top-level `"v":1`. 66 + 67 + ### D6 — Screenshots via `xcap`, capturing our own window by process id 68 + 69 + `xcap` is the maintained cross-platform (Windows/macOS/X11, partial Wayland) window-capture crate; the server finds its own window by matching `xcap::Window` pid to `std::process::id()` and writes a PNG. Alternatives: per-OS capture code (three implementations to maintain — rejected); in-GPUI offscreen render (no public API — impossible today). Capture runs off the main thread (it is OS-level, needs no GPUI state beyond nothing at all), so a slow capture cannot hitch the UI. macOS requires a one-time Screen Recording permission; Wayland behavior is compositor-dependent — both documented, and `screenshot` failing returns `ok:false` without affecting other commands. 70 + 71 + ### D7 — Gating: `devtools` cargo feature AND explicit opt-in at launch 72 + 73 + The server module, `xcap`, `serde_json`, and the protocol types compile only under a `devtools` cargo feature (off by default, not in release artifacts). Even in a devtools build, the server starts only when `TRAWLER_DEVTOOLS=1` is set, so a devtools-built binary run normally has no open sockets. Two layers because a compile-time gate alone makes "I built with the feature for something else" silently listening, and a runtime gate alone ships dead automation code in releases. 74 + 75 + ### D8 — Fixture graphs: builder in `trawler-core` behind a `fixtures` feature 76 + 77 + A `fixtures` module in `trawler-core` (enabled via a `fixtures` cargo feature; the `trawler` crate enables it from `devtools` and from dev-dependencies) exposes a builder that constructs deterministic content through the real `GraphStorage`/mutation APIs: named pages, journal pages on fixed dates, nested outlines, tags, references, properties, and a query block. Deterministic means fixed dates and stable ordering — no clock, no randomness — so a `dump` or screenshot of a fixture graph is comparable across machines. A tiny binary entry point (`cargo run -p trawler --features devtools -- --seed-fixtures <dir>` or equivalent) seeds a scratch dir for `TRAWLER_GRAPH_DIR` sessions; gpui tests call the builder directly against a tempdir. Alternative — checked-in `snapshot.loro` binary fixtures — rejected: opaque in review, brittle across Loro version bumps, and can't parameterize. 78 + 79 + ### D9 — gpui tests construct the real `TrawlerApp` in a test window 80 + 81 + Tests use `#[gpui::test]` + `VisualTestContext`, open a window hosting `TrawlerApp::new(fixture_tempdir, window, cx)` after running the same `editor::init`/`init_keymap` as `main()`, then drive with `simulate_keystrokes`/`simulate_input` and assert on entity state (block tree shape, focus, cursor) — the same state `dump` serializes, keeping the two mechanisms honest with each other. Initial coverage is the README keyboard table: Enter splits, Shift+Enter inlines a newline, Tab/Shift+Tab indent/outdent, Backspace-at-start merges, Alt+Up/Down reorders, Up/Down moves focus at content boundaries, `[[`/`#` opens completion, Escape dismisses it. Testing extracted view-model functions instead was rejected: the value is verifying the binding→action→state pipeline users actually hit. 82 + 83 + ## Risks / Trade-offs 84 + 85 + - [`main.rs` may need refactoring for testability — `TrawlerApp::new` or keymap init may have hidden window/platform assumptions the fake test platform violates] → Land the harness with one smoke test first (open fixture window, type a character, assert it landed) before writing the behavior matrix; treat any refactor it forces as in-scope for `ui-test-harness`. 86 + - [`xcap` captures a black/blank frame on some setups (GPU swapchain content, Wayland compositors)] → Screenshot is one command of five, isolated: it fails soft with `ok:false` and a reason; everything else remains functional. Documented as best-effort in the README section this change adds. 87 + - [The `dump` schema becomes a de-facto API that drifts as the UI evolves] → Version field from day one; schema is defined in one serde module colocated with the state it serializes, so UI refactors that break it fail to compile rather than silently emitting stale shapes. 88 + - [Dev server code in `main.rs` worsens the existing monolith] → Server lives in its own `devtools.rs` module with a narrow seam: one `init(window_handle, cx)` call site in `main()` under `#[cfg(feature = "devtools")]`. 89 + - [Keystroke injection races async work (query re-eval, journal rollover timer)] → Replies are sent after the dispatched action returns, and `dump` reads current state at time of call; clients poll `dump` for settled state rather than the server guessing quiescence. If this proves insufficient, a `settled` command that awaits pending foreground tasks is the escape hatch (open question O2). 90 + - [`gpui 0.2.2` test-support has rough edges on the published crate vs. Zed's in-tree usage] → The smoke test in task one surfaces this immediately; worst case the harness pins what works and documents gaps rather than blocking the dev server track. 91 + 92 + ## Open Questions 93 + 94 + - **O1 — `type` implementation detail**: whether text insertion goes through per-character `dispatch_keystroke` or the platform text-input (IME) path; decide during implementation by whichever matches what real typing exercises in `BlockEditor`. 95 + - **O2 — quiescence**: is "reply after dispatch + client polls `dump`" sufficient for the query-block debounce (~hundreds of ms) workflows, or do we need a `settled`/`wait-idle` command in v1? 96 + - **O3 — fixture scope**: exact fixture inventory (how many pages/blocks, which query examples) — small enough to eyeball in a screenshot, rich enough to exercise references, tags, properties, and a query block. Proposed starting point: one journal page (fixed date), two named pages, ~20 blocks, one query block; refine while implementing.
+34
openspec/changes/add-dev-automation/proposal.md
··· 1 + # Proposal: add-dev-automation 2 + 3 + ## Why 4 + 5 + Trawler is a GPUI desktop app — no DOM, no accessibility tree, no browser to attach to — so today the only way to verify UI behavior or visual changes is a human manually driving the app. That blocks two things: agent-assisted development (an AI collaborator cannot see or interact with the running app while working on styling/layout, which is the immediate roadmap), and automated regression coverage of the keyboard-driven editor semantics that define the product. Every existing test stops below the UI layer (`trawler-core` is headless); the ~2,800-line UI crate has zero tests. 6 + 7 + Cross-platform matters: contributions are opening up to developers on other platforms, so the tooling must be Rust-native and identical everywhere — not per-OS scripts (PowerShell/AutoHotkey-style automation rots and excludes non-Windows contributors). 8 + 9 + ## What Changes 10 + 11 + - Enable gpui's `test-support` feature for the `trawler` crate and add the first `#[gpui::test]` integration tests, driving the real `TrawlerApp` view headlessly via simulated keystrokes and asserting on editor/outline state. Initial coverage targets the documented keyboard behaviors (block split/merge, indent/outdent, focus movement, completion popups). 12 + - Add a feature-gated (`devtools`) dev automation server to the `trawler` app: a localhost-only TCP listener speaking JSON lines, with commands to inject keystrokes/text through GPUI's own dispatch (`Window::dispatch_keystroke`), dump structured UI state (current page, visible blocks, focus, cursor, popups, window bounds), and capture a PNG screenshot of the app window (via the `xcap` crate; best-effort per platform — Wayland explicitly a non-goal). 13 + - Add a fixture-graph builder to `trawler-core` (behind a feature) that seeds a known, deterministic graph — used by both the gpui tests and dev-server sessions (`TRAWLER_GRAPH_DIR` scratch graphs), so interactive screenshots and test assertions run against the same reproducible content. 14 + - The `devtools` feature is off by default and excluded from release builds; no user-facing behavior of the app changes. 15 + 16 + ## Capabilities 17 + 18 + ### New Capabilities 19 + 20 + - `ui-test-harness`: headless gpui integration testing of the real app view — simulated keyboard input, deterministic assertions on outline/editor state, runs on all platforms and CI. 21 + - `dev-automation-server`: feature-gated local automation endpoint on the running app — keystroke/text injection, structured state dump (including window bounds), and window screenshot capture over a JSON-lines TCP protocol. 22 + - `fixture-graphs`: deterministic seeded graph construction in `trawler-core`, shared by tests and interactive dev sessions. 23 + 24 + ### Modified Capabilities 25 + 26 + None — existing capability requirements (block-graph, outline-editor, journal, graph-navigation, search, steel-queries) are unchanged; this is additive dev tooling. 27 + 28 + ## Impact 29 + 30 + - **Crates**: `trawler` gains a `devtools` cargo feature (dev server module, `xcap` + JSON dependencies gated behind it) and dev-dependencies on gpui `test-support`; `trawler-core` gains a fixture/feature module for seeded graphs. 31 + - **New dependencies**: `xcap` (screenshot capture), `serde`/`serde_json` for the wire protocol (devtools-gated), possibly an async channel primitive already available via gpui's executor. 32 + - **Security surface**: the dev server binds `127.0.0.1` only, exists only in `devtools` builds, and is never compiled into release artifacts. 33 + - **CI/workflow**: `cargo test --workspace` picks up the new gpui tests on every platform; contributors and agents get a documented, uniform way to drive and observe the app. 34 + - **Platform caveats**: screenshot capture requires a one-time Screen Recording permission on macOS and is compositor-dependent on Linux Wayland (documented as best-effort); all other dev-server commands are pure GPUI and platform-independent.
+67
openspec/changes/add-dev-automation/specs/dev-automation-server/spec.md
··· 1 + # dev-automation-server 2 + 3 + A feature-gated local automation endpoint on the running app: keystroke/text injection through GPUI's own dispatch, structured UI state dumps (including window bounds), and window screenshot capture, over a JSON-lines TCP protocol on localhost. Gives agents and contributors on any platform an identical, Rust-native way to drive and observe trawler during development. 4 + 5 + ## ADDED Requirements 6 + 7 + ### Requirement: Server is doubly gated and localhost-only 8 + The automation server and its dependencies SHALL compile only under a `devtools` cargo feature that is off by default, and even in a devtools build SHALL start only when explicitly enabled at launch (`TRAWLER_DEVTOOLS=1`). The listener MUST bind to `127.0.0.1` only. Release builds MUST NOT contain the server. 9 + 10 + #### Scenario: Devtools build without opt-in 11 + - **WHEN** a binary built with `--features devtools` is launched without `TRAWLER_DEVTOOLS=1` 12 + - **THEN** no listener is started and no automation port file is written 13 + 14 + #### Scenario: Default build 15 + - **WHEN** the crate is built without the `devtools` feature 16 + - **THEN** the server module, `xcap`, and the wire-protocol dependencies are not compiled into the binary 17 + 18 + ### Requirement: Port discovery 19 + When the server starts it SHALL bind an OS-assigned port, print the port to stdout, and write it to a `devtools.port` file in the graph directory, so clients can discover the endpoint without configuration. 20 + 21 + #### Scenario: Client discovers the endpoint 22 + - **WHEN** the app starts with the server enabled using graph directory `G` 23 + - **THEN** `G/devtools.port` contains the bound port number, and connecting to `127.0.0.1:<port>` succeeds 24 + 25 + ### Requirement: JSON-lines request/response protocol 26 + The server SHALL accept one JSON request object per line and reply with exactly one JSON response object per line, in request order. Failures (unknown command, malformed JSON, command error) MUST produce `{"ok":false,"error":...}` responses and MUST NOT crash or otherwise affect the app. 27 + 28 + #### Scenario: Malformed request 29 + - **WHEN** a client sends a line that is not valid JSON or names an unknown `cmd` 30 + - **THEN** the server replies `{"ok":false,"error":...}` on one line and continues serving subsequent requests, with the app unaffected 31 + 32 + ### Requirement: Keystroke injection through GPUI dispatch 33 + A `keys` command SHALL parse its argument with gpui's `Keystroke::parse` syntax (e.g. `"ctrl-k"`) and dispatch it on the app window via `Window::dispatch_keystroke` on the main thread, replying only after the dispatch completes. A `type` command SHALL insert literal text into the focused editor as real typing would. Injection MUST NOT depend on OS input APIs or on the window having OS focus. 34 + 35 + #### Scenario: Driving quick-open without OS focus 36 + - **WHEN** the trawler window is unfocused or occluded and a client sends `{"cmd":"keys","keys":"ctrl-k"}` 37 + - **THEN** the quick-open switcher opens exactly as if the user pressed Ctrl+K, and the reply `{"ok":true}` arrives after the action has run 38 + 39 + #### Scenario: Typing text 40 + - **WHEN** a block is focused and a client sends `{"cmd":"type","text":"hello [["}` 41 + - **THEN** the focused block receives the text through the same input path as real typing, including triggering reference completion for `[[` 42 + 43 + #### Scenario: Invalid keystroke string 44 + - **WHEN** a client sends a `keys` command whose string `Keystroke::parse` rejects 45 + - **THEN** the server replies `{"ok":false,"error":...}` and dispatches nothing 46 + 47 + ### Requirement: Structured state dump 48 + A `dump` command SHALL return a versioned JSON document (top-level `"v":1`) of semantic UI state derived from entity state, including at minimum: the current view (journal date, page name, or search), the visible outline as a nested block list (id, content, depth, collapsed), the focused block id, cursor offset and selection, any open popup with its contents (completion candidates, quick-open items, calendar month), and the window bounds with scale factor. 49 + 50 + #### Scenario: Dump reflects a completion popup 51 + - **WHEN** reference completion is open with candidates and a client sends `{"cmd":"dump"}` 52 + - **THEN** the response includes the open popup type and its candidate list, the focused block id, and the cursor position within it 53 + 54 + #### Scenario: Bounds available without a screenshot 55 + - **WHEN** a client sends `{"cmd":"bounds"}` (or reads bounds from a `dump`) 56 + - **THEN** the response contains the window's current position, size, and scale factor as reported by gpui 57 + 58 + ### Requirement: Window screenshot capture 59 + A `screenshot` command SHALL capture the app's own window (located by process id) to a PNG at the requested path using cross-platform Rust capture (`xcap`), running the capture off the GPUI main thread. Screenshot support is best-effort per platform: on platforms or compositors where capture fails (notably Linux Wayland), the command MUST fail soft with `{"ok":false,"error":...}` while all other commands remain functional. 60 + 61 + #### Scenario: Successful capture 62 + - **WHEN** a client sends `{"cmd":"screenshot","path":"shot.png"}` on a platform with working capture 63 + - **THEN** a PNG image of the trawler window is written to that path and the reply includes `{"ok":true}` with the resolved path 64 + 65 + #### Scenario: Capture unavailable 66 + - **WHEN** the same command runs where window capture is unsupported 67 + - **THEN** the reply is `{"ok":false,"error":...}` and subsequent `keys`/`type`/`dump` commands still work
+41
openspec/changes/add-dev-automation/specs/fixture-graphs/spec.md
··· 1 + # fixture-graphs 2 + 3 + Deterministic seeded graph construction in `trawler-core`, shared by the gpui test harness and interactive dev-server sessions, so state assertions and screenshots run against identical, reproducible content on every machine. 4 + 5 + ## ADDED Requirements 6 + 7 + ### Requirement: Fixture builder in trawler-core behind a feature 8 + `trawler-core` SHALL provide a fixture-graph builder, compiled only under a `fixtures` cargo feature (off by default), that constructs graph content through the real storage and mutation APIs — not by writing storage files directly. 9 + 10 + #### Scenario: Builder uses real APIs 11 + - **WHEN** the fixture builder seeds a graph directory 12 + - **THEN** the resulting directory is a valid trawler graph (openable by `GraphStorage`), with references, tags, and properties indexed exactly as if the content had been entered through the app 13 + 14 + #### Scenario: Feature-gated compilation 15 + - **WHEN** `trawler-core` is built without the `fixtures` feature 16 + - **THEN** the fixture module and its code are not compiled 17 + 18 + ### Requirement: Fixture content is deterministic 19 + Seeding the standard fixture SHALL produce equivalent graph content on every invocation and every machine: fixed journal dates (no clock reads), stable block ordering, and no randomness, so dumps and screenshots of a fixture graph are comparable across sessions. 20 + 21 + #### Scenario: Two seeds are equivalent 22 + - **WHEN** the standard fixture is seeded into two different directories, on the same or different machines 23 + - **THEN** both graphs contain the same pages, block tree shapes, block contents, tags, references, and properties 24 + 25 + ### Requirement: Standard fixture exercises core features 26 + The standard fixture SHALL include at minimum: a journal page on a fixed date, at least two named pages, a nested outline several levels deep, blocks carrying tags, page references, and typed properties, and one query block — small enough to inspect in a single screenshot, rich enough to exercise references, tags, properties, and query rendering. 27 + 28 + #### Scenario: Fixture supports a query-block screenshot 29 + - **WHEN** the app opens a freshly seeded standard fixture and navigates to the page holding the query block 30 + - **THEN** the query block evaluates against fixture content and renders a non-empty result 31 + 32 + ### Requirement: Seeding available to both consumers 33 + The fixture builder SHALL be callable in-process by gpui tests (against a tempdir) and invocable from the command line to seed a scratch directory for dev-server sessions (a `devtools`-gated flag on the trawler binary, e.g. `--seed-fixtures <dir>`), producing the same standard fixture through both paths. 34 + 35 + #### Scenario: Test consumes fixture in-process 36 + - **WHEN** a `#[gpui::test]` requests a seeded fixture graph in a tempdir 37 + - **THEN** the builder returns a ready graph directory the test opens `TrawlerApp` against, with no external process involved 38 + 39 + #### Scenario: Dev session consumes fixture via CLI 40 + - **WHEN** a contributor runs the seed command against a scratch directory and launches trawler with `TRAWLER_GRAPH_DIR` pointing at it 41 + - **THEN** the app opens the same standard fixture content the tests use
+49
openspec/changes/add-dev-automation/specs/ui-test-harness/spec.md
··· 1 + # ui-test-harness 2 + 3 + Headless gpui integration testing of the real `TrawlerApp` view: simulated keyboard input through the real keymap, deterministic assertions on outline/editor entity state, running identically on all platforms and in CI. 4 + 5 + ## ADDED Requirements 6 + 7 + ### Requirement: Tests drive the real app view headlessly 8 + The test harness SHALL construct the real `TrawlerApp` view (after the same `editor::init` and keymap initialization as `main()`) inside a gpui test window backed by the fake test platform, against a temporary fixture graph directory, with no OS window, GPU, or display required. 9 + 10 + #### Scenario: Smoke test on a fresh fixture graph 11 + - **WHEN** a `#[gpui::test]` opens a test window hosting `TrawlerApp` over a fixture graph in a tempdir and simulates typing a character 12 + - **THEN** the focused block's content contains that character, and the test passes without any real window or GPU being created 13 + 14 + #### Scenario: Platform-independent execution 15 + - **WHEN** `cargo test --workspace` runs on any supported development platform (including CI without a display) 16 + - **THEN** the gpui integration tests execute and produce the same results as on any other platform 17 + 18 + ### Requirement: Keyboard input goes through the real keymap 19 + Simulated input in tests MUST be dispatched as keystrokes through gpui's keymap dispatch (`simulate_keystrokes`/`simulate_input`), not by calling action handlers or editor methods directly, so that a passing test implies the user-visible key binding works. 20 + 21 + #### Scenario: A rebound key would fail the test 22 + - **WHEN** a test simulates `tab` on a focused block and the `tab` binding were removed from the keymap 23 + - **THEN** the test fails, because no action fires through dispatch 24 + 25 + ### Requirement: Documented editor keyboard behaviors are covered 26 + The harness SHALL include passing tests for each documented while-editing keyboard behavior: Enter splits a block at the cursor into a new sibling; Shift+Enter inserts a newline within the block; Tab indents under the previous sibling and Shift+Tab outdents; Backspace at the start of a block merges into the end of the previous sibling; Alt+Up/Alt+Down move a block among its siblings; Up/Down at a content boundary move focus to the previous/next visible block; `[[` and `#` open reference/tag completion; Escape dismisses an open completion popup. 27 + 28 + #### Scenario: Enter splits a block 29 + - **WHEN** a test places the cursor mid-content in a focused block and simulates `enter` 30 + - **THEN** the block tree contains a new following sibling holding the content after the cursor, focus is on the new sibling, and the cursor is at its start 31 + 32 + #### Scenario: Backspace at start merges blocks 33 + - **WHEN** a test focuses a block with a previous sibling, places the cursor at offset 0, and simulates `backspace` 34 + - **THEN** the block's content is appended to the previous sibling, the block is removed from the tree, and the cursor sits at the join point in the merged block 35 + 36 + #### Scenario: Tab indents under previous sibling 37 + - **WHEN** a test focuses a block that has a previous sibling and simulates `tab` 38 + - **THEN** the block becomes the last child of that previous sibling and remains focused 39 + 40 + #### Scenario: Completion popup opens and dismisses 41 + - **WHEN** a test types `[[` in a focused block, then simulates `escape` 42 + - **THEN** the completion popup state is open with candidates after `[[`, and closed after `escape`, with block content preserved 43 + 44 + ### Requirement: Assertions read entity state, not pixels 45 + Tests SHALL assert on the app's entity state (block tree shape and content, focused block, cursor offset, selection, popup state) rather than on rendered output, and this asserted state MUST be the same state the dev-automation-server `dump` command serializes. 46 + 47 + #### Scenario: Structural assertion after a mutation 48 + - **WHEN** a test performs an indent and asserts the result 49 + - **THEN** the assertion inspects the block tree parent/child relationships from entity state, not rendered element geometry
+36
openspec/changes/add-dev-automation/tasks.md
··· 1 + # Tasks: add-dev-automation 2 + 3 + ## 1. Fixture graphs (trawler-core) 4 + 5 + - [ ] 1.1 Add a `fixtures` cargo feature to `trawler-core` and a `fixtures` module scaffold gated behind it 6 + - [ ] 1.2 Implement the fixture builder against the real `GraphStorage`/mutation APIs: named pages, fixed-date journal page, nested outline, tags, references, typed properties, one query block (design O3 starting inventory: one journal page, two named pages, ~20 blocks) 7 + - [ ] 1.3 Add determinism test: seed twice into separate tempdirs, reopen both, assert equivalent pages/trees/content/tags/references/properties (no clock reads, no randomness) 8 + - [ ] 1.4 Add a test asserting a freshly seeded fixture is a valid graph — reopen through `GraphStorage`, verify indexes see the fixture's references/tags, and the fixture query block's expression evaluates to a non-empty result via the query engine 9 + 10 + ## 2. UI test harness (gpui test-support) 11 + 12 + - [ ] 2.1 Wire dev-dependencies: gpui `test-support` feature for the `trawler` crate, `trawler-core` with `fixtures` enabled in dev-deps; extract/expose whatever `main()` init the tests must share (`editor::init`, keymap init) 13 + - [ ] 2.2 Land the smoke test: `#[gpui::test]` opens a test window hosting `TrawlerApp` over a seeded fixture tempdir, simulates typing one character, asserts it landed in the focused block's content — resolve any `TrawlerApp::new`/keymap assumptions the fake test platform surfaces (design risk 1) 14 + - [ ] 2.3 Build shared test helpers: open-app-on-fixture setup, keystroke-driving wrappers, and entity-state assertion helpers for block tree shape, focus, cursor, and popup state 15 + - [ ] 2.4 Cover block structure edits: Enter splits at cursor, Shift+Enter inserts newline, Backspace-at-start merges into previous sibling 16 + - [ ] 2.5 Cover outline movement: Tab indents under previous sibling, Shift+Tab outdents, Alt+Up/Alt+Down reorder among siblings 17 + - [ ] 2.6 Cover focus movement: Up/Down at content boundaries move focus to previous/next visible block 18 + - [ ] 2.7 Cover completion: `[[` and `#` open completion with candidates from fixture content, Escape dismisses with content preserved 19 + - [ ] 2.8 Verify the full suite passes headlessly via `cargo test --workspace` and stays warning-clean under `cargo clippy --workspace --all-targets -- -D warnings` 20 + 21 + ## 3. Dev automation server (trawler, `devtools` feature) 22 + 23 + - [ ] 3.1 Add the `devtools` cargo feature to the `trawler` crate gating a new `devtools` module plus `xcap`/`serde`/`serde_json` dependencies; verify a default build compiles none of it 24 + - [ ] 3.2 Implement server lifecycle: start only when `TRAWLER_DEVTOOLS=1`, bind `127.0.0.1:0` on a listener thread, print the port to stdout and write `<graph-dir>/devtools.port` 25 + - [ ] 3.3 Implement the marshaling seam: JSON-lines framing on the listener thread, commands + oneshot reply senders into a `futures::channel::mpsc` unbounded channel, one foreground task (`cx.spawn`) draining it against the window handle; malformed/unknown requests answer `{"ok":false,...}` without crashing 26 + - [ ] 3.4 Implement `keys` (parse via `Keystroke::parse`, dispatch via `Window::dispatch_keystroke`, reply after dispatch; parse failure → error reply) and `type` (text through the focused editor's real input path — resolve design O1) 27 + - [ ] 3.5 Implement `bounds` and versioned `dump` (`"v":1`): current view, visible outline (id/content/depth/collapsed), focused block, cursor/selection, open popup contents, window bounds + scale — as a serde module colocated with the entity state it serializes 28 + - [ ] 3.6 Implement `screenshot`: locate own window via `xcap` by process id, capture to PNG off the main thread, fail soft with an error reply when capture is unavailable 29 + - [ ] 3.7 Add integration test(s) for the protocol seam where feasible without a real window (framing, error replies, port-file write); manually verify keys/type/dump/screenshot against a fixture-seeded scratch graph on Windows 30 + - [ ] 3.8 Add the `--seed-fixtures <dir>` (devtools-gated) CLI path on the trawler binary invoking the trawler-core fixture builder 31 + 32 + ## 4. Documentation and workflow packaging 33 + 34 + - [ ] 4.1 README section: enabling devtools builds, launching with `TRAWLER_DEVTOOLS=1` + `TRAWLER_GRAPH_DIR` scratch graphs, seeding fixtures, the command protocol with examples, and per-platform screenshot caveats (macOS Screen Recording permission, Wayland best-effort) 35 + - [ ] 4.2 Add a project `run`/dev-loop skill (`.claude/skills/`) encoding the agent workflow: seed fixture scratch graph → kill stale app → build with devtools → launch → drive via socket → screenshot → cleanup, including the "running exe blocks cargo rebuild" kill step 36 + - [ ] 4.3 Reconcile design.md open questions (O1 `type` path, O2 quiescence, O3 fixture inventory) with what was actually built; record any deviations