diff --git a/openspec/changes/add-dev-automation/.openspec.yaml b/openspec/changes/archive/2026-07-14-add-dev-automation/.openspec.yaml similarity index 100% rename from openspec/changes/add-dev-automation/.openspec.yaml rename to openspec/changes/archive/2026-07-14-add-dev-automation/.openspec.yaml diff --git a/openspec/changes/add-dev-automation/design.md b/openspec/changes/archive/2026-07-14-add-dev-automation/design.md similarity index 100% rename from openspec/changes/add-dev-automation/design.md rename to openspec/changes/archive/2026-07-14-add-dev-automation/design.md diff --git a/openspec/changes/add-dev-automation/proposal.md b/openspec/changes/archive/2026-07-14-add-dev-automation/proposal.md similarity index 100% rename from openspec/changes/add-dev-automation/proposal.md rename to openspec/changes/archive/2026-07-14-add-dev-automation/proposal.md diff --git a/openspec/changes/add-dev-automation/specs/dev-automation-server/spec.md b/openspec/changes/archive/2026-07-14-add-dev-automation/specs/dev-automation-server/spec.md similarity index 100% rename from openspec/changes/add-dev-automation/specs/dev-automation-server/spec.md rename to openspec/changes/archive/2026-07-14-add-dev-automation/specs/dev-automation-server/spec.md diff --git a/openspec/changes/add-dev-automation/specs/fixture-graphs/spec.md b/openspec/changes/archive/2026-07-14-add-dev-automation/specs/fixture-graphs/spec.md similarity index 100% rename from openspec/changes/add-dev-automation/specs/fixture-graphs/spec.md rename to openspec/changes/archive/2026-07-14-add-dev-automation/specs/fixture-graphs/spec.md diff --git a/openspec/changes/add-dev-automation/specs/ui-test-harness/spec.md b/openspec/changes/archive/2026-07-14-add-dev-automation/specs/ui-test-harness/spec.md similarity index 100% rename from openspec/changes/add-dev-automation/specs/ui-test-harness/spec.md rename to openspec/changes/archive/2026-07-14-add-dev-automation/specs/ui-test-harness/spec.md diff --git a/openspec/changes/add-dev-automation/tasks.md b/openspec/changes/archive/2026-07-14-add-dev-automation/tasks.md similarity index 100% rename from openspec/changes/add-dev-automation/tasks.md rename to openspec/changes/archive/2026-07-14-add-dev-automation/tasks.md diff --git a/openspec/specs/dev-automation-server/spec.md b/openspec/specs/dev-automation-server/spec.md new file mode 100644 index 0000000..89bf88d --- /dev/null +++ b/openspec/specs/dev-automation-server/spec.md @@ -0,0 +1,70 @@ +# dev-automation-server Specification + +## Purpose + + +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. + +## Requirements + +### Requirement: Server is doubly gated and localhost-only +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. + +#### Scenario: Devtools build without opt-in +- **WHEN** a binary built with `--features devtools` is launched without `TRAWLER_DEVTOOLS=1` +- **THEN** no listener is started and no automation port file is written + +#### Scenario: Default build +- **WHEN** the crate is built without the `devtools` feature +- **THEN** the server module, `xcap`, and the wire-protocol dependencies are not compiled into the binary + +### Requirement: Port discovery +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. + +#### Scenario: Client discovers the endpoint +- **WHEN** the app starts with the server enabled using graph directory `G` +- **THEN** `G/devtools.port` contains the bound port number, and connecting to `127.0.0.1:` succeeds + +### Requirement: JSON-lines request/response protocol +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. + +#### Scenario: Malformed request +- **WHEN** a client sends a line that is not valid JSON or names an unknown `cmd` +- **THEN** the server replies `{"ok":false,"error":...}` on one line and continues serving subsequent requests, with the app unaffected + +### Requirement: Keystroke injection through GPUI dispatch +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. + +#### Scenario: Driving quick-open without OS focus +- **WHEN** the trawler window is unfocused or occluded and a client sends `{"cmd":"keys","keys":"ctrl-k"}` +- **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 + +#### Scenario: Typing text +- **WHEN** a block is focused and a client sends `{"cmd":"type","text":"hello [["}` +- **THEN** the focused block receives the text through the same input path as real typing, including triggering reference completion for `[[` + +#### Scenario: Invalid keystroke string +- **WHEN** a client sends a `keys` command whose string `Keystroke::parse` rejects +- **THEN** the server replies `{"ok":false,"error":...}` and dispatches nothing + +### Requirement: Structured state dump +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. + +#### Scenario: Dump reflects a completion popup +- **WHEN** reference completion is open with candidates and a client sends `{"cmd":"dump"}` +- **THEN** the response includes the open popup type and its candidate list, the focused block id, and the cursor position within it + +#### Scenario: Bounds available without a screenshot +- **WHEN** a client sends `{"cmd":"bounds"}` (or reads bounds from a `dump`) +- **THEN** the response contains the window's current position, size, and scale factor as reported by gpui + +### Requirement: Window screenshot capture +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. + +#### Scenario: Successful capture +- **WHEN** a client sends `{"cmd":"screenshot","path":"shot.png"}` on a platform with working capture +- **THEN** a PNG image of the trawler window is written to that path and the reply includes `{"ok":true}` with the resolved path + +#### Scenario: Capture unavailable +- **WHEN** the same command runs where window capture is unsupported +- **THEN** the reply is `{"ok":false,"error":...}` and subsequent `keys`/`type`/`dump` commands still work diff --git a/openspec/specs/fixture-graphs/spec.md b/openspec/specs/fixture-graphs/spec.md new file mode 100644 index 0000000..731e950 --- /dev/null +++ b/openspec/specs/fixture-graphs/spec.md @@ -0,0 +1,44 @@ +# fixture-graphs Specification + +## Purpose + + +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. + +## Requirements + +### Requirement: Fixture builder in trawler-core behind a feature +`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. + +#### Scenario: Builder uses real APIs +- **WHEN** the fixture builder seeds a graph directory +- **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 + +#### Scenario: Feature-gated compilation +- **WHEN** `trawler-core` is built without the `fixtures` feature +- **THEN** the fixture module and its code are not compiled + +### Requirement: Fixture content is deterministic +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. + +#### Scenario: Two seeds are equivalent +- **WHEN** the standard fixture is seeded into two different directories, on the same or different machines +- **THEN** both graphs contain the same pages, block tree shapes, block contents, tags, references, and properties + +### Requirement: Standard fixture exercises core features +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. + +#### Scenario: Fixture supports a query-block screenshot +- **WHEN** the app opens a freshly seeded standard fixture and navigates to the page holding the query block +- **THEN** the query block evaluates against fixture content and renders a non-empty result + +### Requirement: Seeding available to both consumers +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 `), producing the same standard fixture through both paths. + +#### Scenario: Test consumes fixture in-process +- **WHEN** a `#[gpui::test]` requests a seeded fixture graph in a tempdir +- **THEN** the builder returns a ready graph directory the test opens `TrawlerApp` against, with no external process involved + +#### Scenario: Dev session consumes fixture via CLI +- **WHEN** a contributor runs the seed command against a scratch directory and launches trawler with `TRAWLER_GRAPH_DIR` pointing at it +- **THEN** the app opens the same standard fixture content the tests use diff --git a/openspec/specs/ui-test-harness/spec.md b/openspec/specs/ui-test-harness/spec.md new file mode 100644 index 0000000..0e0d50e --- /dev/null +++ b/openspec/specs/ui-test-harness/spec.md @@ -0,0 +1,52 @@ +# ui-test-harness Specification + +## Purpose + + +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. + +## Requirements + +### Requirement: Tests drive the real app view headlessly +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. + +#### Scenario: Smoke test on a fresh fixture graph +- **WHEN** a `#[gpui::test]` opens a test window hosting `TrawlerApp` over a fixture graph in a tempdir and simulates typing a character +- **THEN** the focused block's content contains that character, and the test passes without any real window or GPU being created + +#### Scenario: Platform-independent execution +- **WHEN** `cargo test --workspace` runs on any supported development platform (including CI without a display) +- **THEN** the gpui integration tests execute and produce the same results as on any other platform + +### Requirement: Keyboard input goes through the real keymap +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. + +#### Scenario: A rebound key would fail the test +- **WHEN** a test simulates `tab` on a focused block and the `tab` binding were removed from the keymap +- **THEN** the test fails, because no action fires through dispatch + +### Requirement: Documented editor keyboard behaviors are covered +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. + +#### Scenario: Enter splits a block +- **WHEN** a test places the cursor mid-content in a focused block and simulates `enter` +- **THEN** the block tree contains a new following sibling holding the content after the cursor, and focus is on the new sibling + +#### Scenario: Backspace at start merges blocks +- **WHEN** a test focuses a block with a previous sibling, places the cursor at offset 0, and simulates `backspace` +- **THEN** the block's content is appended to the previous sibling, the block is removed from the tree, and focus moves to the merged block + +#### Scenario: Tab indents under previous sibling +- **WHEN** a test focuses a block that has a previous sibling and simulates `tab` +- **THEN** the block becomes the last child of that previous sibling and remains focused + +#### Scenario: Completion popup opens and dismisses +- **WHEN** a test types `[[` in a focused block, then simulates `escape` +- **THEN** the completion popup state is open with candidates after `[[`, and closed after `escape`, with block content preserved + +### Requirement: Assertions read entity state, not pixels +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. + +#### Scenario: Structural assertion after a mutation +- **WHEN** a test performs an indent and asserts the result +- **THEN** the assertion inspects the block tree parent/child relationships from entity state, not rendered element geometry