Files
ddidderr 30863e1f0b test(gtk): live D-Bus/portal comparison against the original Ghostty
Adds scripts/live-dbus-test.sh (`just live-dbus`): each scenario runs
the installed Ghostty 1.3.1 and the Rust build on a private
dbus-run-session bus inside a private headless weston (the user's
session bus is never written to), with Python/Gio mocks for
org.freedesktop.Notifications, the GlobalShortcuts/Settings portal and
org.freedesktop.systemd1, plus a real xdg-desktop-portal run. Scenarios:
single-instance (+detect), +new-window with arguments, notifications,
global shortcuts, color-scheme settings, cgroup scope creation, URL
opening. tests/live_dbus.rs compares the Rust build against goldens
recorded from the original (skips when tools or a fresh production
build are missing).

Two Rust bugs found and fixed (no upstream deviation):
- GlobalShortcuts CreateSession/BindShortcuts payloads used the wrong
  D-Bus signatures ((v), double-boxed values), which a real portal
  rejects; signature tests added.
- CreateSession was sent twice at startup; the config is now bound only
  after the D-Bus connection is set, as in Zig.

All nine scenarios now behave identically to the original
(docs/rewrite/notes/live-dbus.md).

Test Plan:
- just live-dbus
- cargo test --test live_dbus (needs target/vp/production/ghostty)
2026-10-12 18:49:53 +02:00

62 lines
6.3 KiB
Markdown

# Live D-Bus / portal integration (agent LIVE)
Harness: `scripts/live-dbus-test.sh` (`just live-dbus [scenario...]`), helpers in `scripts/live-dbus/`
(`scenario.sh`, `scenarios.sh`, `mock.py`, `normalize.py`, child scripts, `golden/`, `build-rust.sh`).
Automated gate: `tests/live_dbus.rs` (golden mode, see below).
## Method
- Every scenario runs once per binary (original `/usr/bin/ghostty` = oracle, Rust release-profile build) on a
**private session bus**: `dbus-run-session --config-file=...` with its own `servicedir` (only
`com.mitchellh.ghostty.service` pointing at the binary under test, so D-Bus activation starts the build under test, not
`/usr/bin/ghostty`), inside a private headless weston (as `visual-parity.sh`), private HOME/XDG dirs,
`GSETTINGS_BACKEND=memory`. The user's bus, portal and desktop are never touched. All spawned processes carry an
`LDB_TAG` env marker and are swept at the end.
- Mock services (`mock.py`, Gio via PyGObject) on the private bus: `org.freedesktop.Notifications`,
`org.freedesktop.systemd1` (Manager.StartTransientUnit, switchable to failing), `org.freedesktop.portal.Desktop`
(GlobalShortcuts incl. Request/Session objects, Settings, OpenURI). Control interface `test.ghostty.Mock`
(`Activate`, `SetColorScheme`, `Invoke`, `SetFail`). Every received call is logged as JSON.
- `busctl --user monitor --json=short` records all traffic; `normalize.py` keeps `org.gtk.*`, Notifications, systemd1 and
portal messages and normalizes pids, tokens, unique names, paths. Observable effects ("facts") come from the exported
GtkApplication window objects (`busctl tree`), process counts, child cwd/argv, and the pty (colour-scheme reports).
- `golden/<scenario>.txt` = the original's facts + received mock calls (recorded with `UPDATE_GOLDEN=1`). With the
original installed the harness compares original vs Rust directly **and** Rust vs golden; without it (CI,
`tests/live_dbus.rs`, `LABELS=rust`) only Rust vs golden. Debug builds use the app id `com.mitchellh.ghostty-debug`
(as Zig), so the Rust build must be the release profile (`scripts/live-dbus/build-rust.sh`).
- Real `xdg-desktop-portal` + `xdg-desktop-portal-gtk` do run on the private bus (scenario `real-portal`, keyfile
gsettings backend so `color-scheme` can be changed live). They cannot give Ghostty an app id without systemd/flatpak
("An app id is required"), so GlobalShortcuts against the real frontend only yields `NotAllowed`; the BindShortcuts
payload is therefore checked against the mock.
## Scenarios: original vs Rust
| Scenario | Covers | Result |
|---|---|---|
| single-instance | first launch owns `com.mitchellh.ghostty`; `+new-window` (plain, `-e`, `--working-directory`, both, wrong/bad `--class`), second GUI launch forwarded, `-e` launch is never single-instance, `--class=com.example.custom-app` own bus name/path and `+new-window --class`; exact `org.gtk.Actions.Activate new-window-command` payloads (the CLI prepends `--working-directory=<cwd>`), exported actions, window counts, child cwd/argv | identical |
| single-instance-detect | `gtk-single-instance=detect`: plain launch = single instance, `TERM_PROGRAM` set or any CLI argument = dedicated instance | identical |
| notifications | OSC 9, OSC 777 (title/body/empty parts/`;` in body/markup/non-ASCII), repeated body (replaces id), `ActionInvoked default` incl. unknown id, `desktop-notifications=false` after reload; exact `Notify(app, replaces_id, icon, summary, body, actions, hints, timeout)` | identical (app name `""`, summary `Ghostty` for OSC 9, actions `[default,""]`, hints `desktop-entry`, `image-path`, `urgency=1`) |
| shortcuts | `global:` keybinds -> `CreateSession`/`BindShortcuts` payload (ids, descriptions, order), `Activated` -> action, unknown id ignored, session `Close` + re-create on reload, none without global binds | **diverged, fixed** (see below) |
| settings | portal `org.freedesktop.appearance color-scheme` (Read/ReadAll/SettingChanged) -> CSI ? 997 reports (mode 2031), `theme = light:..,dark:..` OSC 11 colour, `window-theme=dark` | identical |
| cgroup | `linux-cgroup` default/always/never, memory/processes limits (`MemoryHigh`,`TasksMax`,`ManagedOOMMemoryPressure`,`PIDs`), failing StartTransientUnit (app keeps running) | identical |
| openuri | `open-config` -> `xdg-open <config>`; **no** portal OpenURI/OpenFile call (upstream TODO in `Application.openUrl`; the Rust port has the same TODO) | identical |
| openurl-click | ctrl+click on plain URL / OSC 8 link / `file://` / path -> `xdg-open` argv | Rust only (the original cannot be driven: no input injection, Xwayland pointer is not reachable in headless weston); golden derived from `Surface.zig`/`os/open.zig` |
| real-portal | real xdg-desktop-portal + gtk backend: Settings color-scheme live changes, GlobalShortcuts error path, Inhibit monitor | identical |
## Divergences found and fixed (Rust port bugs, not upstream bugs: normal commits)
1. **GlobalShortcuts payloads had the wrong D-Bus signature** (`crates/ghostty-apprt-gtk/src/class/global_shortcuts.rs`):
`CreateSession` was sent as `(v)` instead of `(a{sv})`, option values were double-boxed (`v` in `v`), and
`BindShortcuts` was `(oa(sv)sv)` instead of `(oa(sa{sv})sa{sv})`. A real portal rejects these, so global shortcuts could
never work. Now built with `tuple_from_iter`/typed arrays by `create_session_payload`/`bind_shortcuts_payload`;
unit tests assert the signatures.
2. **CreateSession was sent twice at startup** (`class/application.rs`): the app's `config` notify forwarded the config to
`GlobalShortcuts` before `startup_global_shortcuts`; Zig binds the property only after `setDbusConnection`
(`bindProperty(.., .sync_create)`), so the session is created once. New `global_shortcuts_bound` flag.
No upstream (deviant) bugs were found in this area; `DEVIANT_FIXES.md` is unchanged by this work.
## Not covered
- Input method (ibus/fcitx not installed) stays in "Not verifiable on this machine".
- The original cannot receive pointer input here, so link clicks are checked for the Rust build only.
- `notify-on-command-finished` never produced a notification for either binary in the headless setup (identical, no
focus change events), so only OSC 9/777 are compared.
- Real GlobalShortcuts backends (GNOME/KDE) are not installed; the real frontend answers `NotAllowed` for non-sandboxed
apps without systemd-assigned app ids.