chore: import upstream snapshot with attribution
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# Linux Compatibility Testing
|
||||
|
||||
*Last updated: 2026-05-03*
|
||||
|
||||
This directory holds the manual test plan for the Linux fork of Claude Desktop. The structure is designed for human readers today and scripted runners tomorrow.
|
||||
|
||||
## Layout
|
||||
|
||||
| Folder / file | Purpose |
|
||||
|---------------|---------|
|
||||
| [`matrix.md`](./matrix.md) | **The dashboard.** Cross-environment results table + per-section env-specific status snapshots. Single source of truth for test status. |
|
||||
| [`runbook.md`](./runbook.md) | How to run a sweep: VM setup, diagnostic capture, status update workflow, severity guidance. |
|
||||
| [`cases/`](./cases/) | Functional test specs grouped by feature surface. Stable IDs: `T###` cross-env, `S###` env-specific. |
|
||||
|
||||
## Environment key
|
||||
|
||||
| Abbrev | Distro | DE | Display server |
|
||||
|--------|--------|-----|----------------|
|
||||
| KDE-W | Fedora 43 | KDE Plasma | Wayland |
|
||||
| KDE-X | Fedora 43 | KDE Plasma | X11 |
|
||||
| GNOME | Fedora 43 | GNOME | Wayland |
|
||||
| Ubu | Ubuntu 24.04 | GNOME | Wayland |
|
||||
| Sway | Fedora 43 | Sway | Wayland (wlroots) |
|
||||
| i3 | Fedora 43 | i3 | X11 |
|
||||
| Niri | Fedora 43 | Niri | Wayland (wlroots) |
|
||||
| Hypr-O | OmarchyOS | Hyprland | Wayland (wlroots) |
|
||||
| Hypr-N | NixOS | Hyprland | Wayland (wlroots) |
|
||||
|
||||
Status legend: `✓` pass · `✗` fail · `🔧` mitigated · `?` untested · `-` N/A
|
||||
|
||||
Cells include linked issue/PR numbers when relevant — e.g. `✗ #404` or `🔧 #406`. A bare `✗` means the failure is verified but no tracking issue is filed yet.
|
||||
|
||||
## Severity tiers
|
||||
|
||||
Each test is tagged with one of:
|
||||
|
||||
| Tier | Meaning | Sweep cadence |
|
||||
|------|---------|---------------|
|
||||
| **Smoke** | Release-gate. Must pass before any tag is cut. | Every release tag, on KDE-W + one wlroots row |
|
||||
| **Critical** | Regression-blocker. Failure on any supported environment blocks the release. | Every release tag, on every active row |
|
||||
| **Should** | Important but not blocking. Track as bugs, fix before next stable. | Quarterly + on demand |
|
||||
| **Could** | Edge cases, nice-to-have. | On demand only |
|
||||
|
||||
## Smoke set
|
||||
|
||||
The minimum set that gates a release. Run on **KDE-W** (daily-driver) plus **Hypr-N** (clean wlroots). Sweep target: ~20 minutes.
|
||||
|
||||
| ID | Surface | One-line check |
|
||||
|----|---------|----------------|
|
||||
| [T01](./cases/launch.md#t01--app-launch) | Launch | App opens; main window renders within ~10s |
|
||||
| [T03](./cases/tray-and-window-chrome.md#t03--tray-icon-present) | Tray | Tray icon appears; click toggles window |
|
||||
| [T04](./cases/tray-and-window-chrome.md#t04--window-decorations-draw) | Window | OS-native frame draws and responds |
|
||||
| [T05](./cases/shortcuts-and-input.md#t05--url-handler-opens-claudeai-links-in-app) | Input | `xdg-open https://claude.ai/...` opens in-app |
|
||||
| [T07](./cases/tray-and-window-chrome.md#t07--in-app-topbar-renders--clickable) | Window | Hybrid topbar renders, every button clicks |
|
||||
| [T08](./cases/tray-and-window-chrome.md#t08--hide-to-tray-on-close) | Window | Close button hides to tray, doesn't quit |
|
||||
| [T11](./cases/extensibility.md#t11--plugin-install-anthropic--partners) | Extensibility | Anthropic & Partners plugin install completes |
|
||||
| [T15](./cases/code-tab-foundations.md#t15--sign-in-completes-via-browser-handoff) | Auth | Sign-in completes via `xdg-open` browser handoff |
|
||||
| [T16](./cases/code-tab-foundations.md#t16--code-tab-loads) | Code tab | Code tab loads (no 403, no blank screen) |
|
||||
| [T17](./cases/code-tab-foundations.md#t17--folder-picker-opens) | Code tab | Folder picker opens via portal/native chooser |
|
||||
|
||||
## Test corpus snapshot
|
||||
|
||||
| Bucket | Count |
|
||||
|--------|-------|
|
||||
| Cross-environment functional (`T###`) | 39 |
|
||||
| Environment-specific functional (`S###`) | 37 |
|
||||
| UI surfaces inventoried | 10 |
|
||||
| Total functional tests | 76 |
|
||||
|
||||
For detailed status by ID, see [`matrix.md`](./matrix.md).
|
||||
|
||||
## Automation status
|
||||
|
||||
Automation is partially landed. The harness lives at
|
||||
[`tools/test-harness/`](../../tools/test-harness/) — twenty Playwright
|
||||
specs wired (T01, T03, T04, T17, S09, S12, S29-S37, plus four H-prefix
|
||||
self-tests), thirteen passing on KDE-W and six skipping cleanly per
|
||||
spec intent. See [`tools/test-harness/README.md`](../../tools/test-harness/README.md)
|
||||
for the live status table, [`automation.md`](./automation.md) for
|
||||
architectural decisions, and the SIGUSR1 / runtime-attach pattern that
|
||||
bypasses the app's CDP auth gate.
|
||||
|
||||
### Grounding sweep + probe
|
||||
|
||||
Separate from the test sweep:
|
||||
[`runbook.md` "Grounding sweep"](./runbook.md#grounding-sweep) covers
|
||||
the workflow for verifying case docs themselves against the live
|
||||
build on every upstream version bump — static anchor pass plus a
|
||||
runtime probe ([`tools/test-harness/grounding-probe.ts`](../../tools/test-harness/grounding-probe.ts))
|
||||
that captures IPC handler registry, accelerator state, autoUpdater
|
||||
gate, AX-tree fingerprint, and other claims static analysis can't
|
||||
disambiguate. Anchor and drift conventions live in
|
||||
[`cases/README.md`](./cases/README.md#anchor-scope).
|
||||
|
||||
The structure remains automation-friendly for new tests:
|
||||
|
||||
1. **Stable test IDs.** `T01`-`T39` and `S01`-`S28` won't move. New tests append. Sequential, not semantic.
|
||||
2. **Standardized test bodies.** Every functional test has `Severity`, `Steps`, `Expected`, `Diagnostics on failure`, and `References` sections. The Steps and Diagnostics fields are scripted-runner-shaped.
|
||||
3. **Per-element UI checklists.** Each UI surface file lists interactive elements in a table — every row is a candidate `webContents.executeJavaScript` / `xprop` / DBus assertion.
|
||||
4. **Severity-driven sweeps.** Tests with a `runner:` field execute via [`tools/test-harness/orchestrator/sweep.sh`](../../tools/test-harness/orchestrator/sweep.sh); JUnit XML lands in `results/results-${ROW}-${DATE}/junit.xml`. Tests without a `runner:` continue to run manually.
|
||||
|
||||
For tests that don't have a runner yet, status updates land in [`matrix.md`](./matrix.md) by hand after each manual sweep. For tests that do, the automation invocation is the source of truth — see [`runbook.md`](./runbook.md#automated-runs).
|
||||
|
||||
## Conventions
|
||||
|
||||
- **One PR per sweep result, not per cell change.** Bundle a full row update into a single commit titled `test: KDE-W sweep $(date +%F)`. Reduces matrix-merge noise.
|
||||
- **Tested-version pin.** Every status update should mention the `claude-desktop` upstream version + the project version (`v1.3.x+claude...`) in the commit. Otherwise a `✓` from six months ago looks current.
|
||||
- **Diagnostics on failure are mandatory.** Don't file `✗` without the captures listed in the test's `Diagnostics on failure` block. The runbook covers how to capture each.
|
||||
- **Issue links go inline.** Status cells link directly to the relevant issue/PR.
|
||||
|
||||
See [`runbook.md`](./runbook.md) for the full mechanics.
|
||||
@@ -0,0 +1,439 @@
|
||||
# Automation Plan
|
||||
|
||||
*Last updated: 2026-04-30*
|
||||
|
||||
> **Status:** Direction agreed; first vertical slice scaffolded at
|
||||
> [`tools/test-harness/`](../../tools/test-harness/) covering T01, T03, T04,
|
||||
> T17 on KDE-W. The [Decisions](#decisions) table captures the calls
|
||||
> already made; [Still open](#still-open) is the short list of things
|
||||
> genuinely undecided. This file will fold into [`README.md`](./README.md)
|
||||
> and [`runbook.md`](./runbook.md) once the harness has run a few real
|
||||
> sweeps.
|
||||
|
||||
The [`README.md`](./README.md) automation roadmap is one paragraph. This file
|
||||
is the longer version — what shape the harness takes, which tools fit which
|
||||
tests, which anti-patterns to design against, and what to build first.
|
||||
|
||||
## Why this exists
|
||||
|
||||
The 67 tests in [`cases/`](./cases/) already have stable IDs and
|
||||
standardized bodies. That structure is unusually friendly to
|
||||
automation — but only if the harness is shaped to match the corpus,
|
||||
rather than the other way around. Three things make that non-trivial:
|
||||
|
||||
1. The tests aren't homogeneous. Some are pure-renderer (Code tab), some are
|
||||
native-OS-level (tray, autostart, URL handler), some are visual/UX checks
|
||||
that probably stay manual forever.
|
||||
2. The matrix is nine environments, four display servers, and two package
|
||||
formats. Input injection on Wayland is genuinely different from X11, and
|
||||
X11 is the project's default backend (Wayland-native is opt-in until
|
||||
portal coverage matures across compositors).
|
||||
3. Many failures are environment-specific by construction (mutter XWayland
|
||||
key-grab, BindShortcuts on Niri, Omarchy Ozone-Wayland env exports). A
|
||||
single "run everything everywhere" harness will mis-skip those.
|
||||
|
||||
## Decisions
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| 1 | **Single language: TypeScript.** Every runner is `.ts`; OS tools are shelled out via `child_process` and wrapped as TS helpers. Python only as a last-resort escape hatch for AT-SPI cases that resist portal mocking. | Playwright Electron is JS-native (post-Spectron); `dbus-next` covers DBus end-to-end; portal mocking removes the dogtail dependency for most native-dialog tests. Three-language overhead doesn't pay back. |
|
||||
| 2 | **Harness location: `tools/test-harness/`.** Sibling to `scripts/`. | Keeps `docs/testing/` documentation-only; matches the project's existing `tools/` / `scripts/` split. |
|
||||
| 3 | **VM images: Packer for imperative distros + Nix flake for `Hypr-N`.** | Packer builds golden snapshots that boot fast and rebuild as code; Nix flake handles NixOS natively without a second wrapper. Vagrant's per-boot provisioning model is the wrong tradeoff for hermetic per-test snapshots. |
|
||||
| 4 | **No CI infrastructure initially.** Harness is invocable from CI (orchestrator is a bash script with `ROW`, `ARTIFACT`, `OUTPUT_DIR` env vars), but sweeps run manually from the dev box for the first ~20 tests. CI wrapper comes after there's signal on which tests are stable enough to run unattended. | Avoids weeks of GHA / nested-KVM debugging for tests that aren't ready to be unattended. The bash orchestrator is the same code either way. |
|
||||
| 5 | **Selectors: semantic locators only (`getByRole`, `getByLabel`, `getByText`).** No CSS classes against minified renderer output. No proactive `data-testid` injection patch. Escalate per-test only when a specific test proves unstable: first ask upstream for a stable `data-testid`; only carry an `app-asar.sh` patch if upstream declines. | Building selector-injection infrastructure up front is a guess at where rot will happen. Modern React apps usually have enough ARIA roles and visible text for `getByRole`/`getByText` to be durable. Measure before patching. |
|
||||
| 6 | **X11-default verification is Smoke. Wayland-native characterization is Should.** Add a Smoke test asserting the launcher log shows X11/XWayland selected on each row (the project's release-gate behavior). Add per-row Should tests characterizing what happens if Electron's default Wayland selection is allowed — these are informational, not release-gating. | The project chose X11 default because portal `GlobalShortcuts` coverage is patchy. The new Wayland-default tests exist to map that landscape, not to gate releases on it. |
|
||||
| 7 | **Diagnostic retention: last 10 greens + all reds, on `main` only.** Captures `--doctor`, launcher log, screenshot every run. Reds retained indefinitely; greens rotate. | Cheap regression-bisect baseline; bounded storage; reds are the things you actually need to look at six weeks later. |
|
||||
| 8 | **JUnit XML lives as workflow-run artifacts.** Each sweep run uploads `results-${ROW}-${DATE}.tar.zst` containing JUnit + diagnostic bundle. Default 90-day retention, extend to 365 if needed. The matrix-regen step downloads the latest run's artifacts and updates `matrix.md` in a PR. | Zero new infrastructure; GH provides storage, lifecycle, auth. If cross-run analytics later require longer history, promote to a separate `claude-desktop-debian-test-history` repo *then* — not before there's signal on what to keep. |
|
||||
|
||||
## The three layers
|
||||
|
||||
Looking at the corpus, every test falls into one of three buckets, and each
|
||||
bucket maps to a different shape of TS code (not a different language):
|
||||
|
||||
| Layer | What it covers | Implementation |
|
||||
|-------|----------------|----------------|
|
||||
| **L1 — Renderer** | Code tab, plugin install, settings, prompt area, slash menu, side chat | `playwright-electron` (`_electron.launch()`) directly |
|
||||
| **L2 — Native / OS** | Tray (DBus), window decorations, URL handler (`xdg-open`), autostart, `--doctor`, multi-instance, hide-to-tray, native file picker (T17) | TS + `dbus-next` for DBus; `child_process` shell-outs wrapped as TS helpers (`xprop`, `wlr-randr`, `swaymsg`, `niri msg`, `pgrep`, `ydotool`); `dbus-next`-driven portal mocking for native-dialog tests |
|
||||
| **L3 — Manual** | "Icon is crisp on HiDPI", drag-and-drop feel, T28 catch-up after suspend (real wall-clock), subjective UX checks | Human eyes; capture in [`runbook.md`](./runbook.md) sweep loop |
|
||||
|
||||
The `runner:` field [`README.md`](./README.md) hints at is the right unit.
|
||||
One TS file per test under `tools/test-harness/runners/`, free to mix L1 and
|
||||
L2 calls within a single test file. Tests without a `runner:` field stay
|
||||
manual indefinitely — that's a feature, not a TODO.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
host (orchestrator) per-row VM (or Nobara host for KDE-W)
|
||||
───────────────────── ──────────────────────────────────────
|
||||
tools/sweep.sh ssh → tools/test-harness/run.ts
|
||||
├── L1 runners (playwright-electron)
|
||||
├── L2 runners (dbus-next + shell-outs)
|
||||
└── junit.xml + diagnostic bundle
|
||||
tools/render-matrix.sh ← scp /tmp/results-${ROW}-${DATE}.tar.zst
|
||||
matrix.md (regenerated)
|
||||
```
|
||||
|
||||
The orchestrator is dumb: copy artifact in, kick the harness, copy results
|
||||
out. Per-row variation lives in `tools/test-images/${ROW}/` (Packer recipe +
|
||||
cloud-init / autoinstall, or a Nix flake for `Hypr-N`). The harness inside
|
||||
each VM is the same checked-in TS code, branched on `XDG_CURRENT_DESKTOP` /
|
||||
`XDG_SESSION_TYPE` for env-specific helpers.
|
||||
|
||||
Result format pivots on **JUnit XML** — well-trodden ground. Several actions
|
||||
already exist that turn JUnit into Markdown summaries
|
||||
([`junit-to-md`](https://github.com/davidahouse/junit-to-md), the
|
||||
[Test Summary Action](https://github.com/marketplace/actions/junit-test-dashboard)).
|
||||
The matrix-regen step is just "download artifact, merge per-row JUnit, render
|
||||
cells, commit a PR."
|
||||
|
||||
### Why not drive Playwright over the wire?
|
||||
|
||||
The obvious sketch is "orchestrator on the host opens a CDP / DevTools port
|
||||
on each VM and runs the whole suite from one place." It looks clean but has
|
||||
real costs:
|
||||
|
||||
- CDP over network is fragile; port forwards are a constant footgun on
|
||||
flaky links.
|
||||
- Doesn't help with L2 at all — DBus calls, `xprop`, `pgrep`, file-system
|
||||
probes still have to run in-VM.
|
||||
- You'd end up maintaining two transports anyway, so the centralization
|
||||
win evaporates.
|
||||
|
||||
In-VM Playwright via `_electron.launch()` is the [official Electron
|
||||
recommendation](https://www.electronjs.org/docs/latest/tutorial/automated-testing)
|
||||
since Spectron was archived in Feb 2022. No remote debug port needed; it
|
||||
spawns Electron directly and gives you a context.
|
||||
|
||||
## Toolchain choices per layer
|
||||
|
||||
### L1 — `playwright-electron`
|
||||
|
||||
- Spawn via `_electron.launch({ args: ['main.js'] })` — no `--remote-debugging-port`.
|
||||
- Gate `nodeIntegration: true` and `contextIsolation: false` behind
|
||||
`process.env.CI === '1'` so tests get full main-process access without
|
||||
weakening production security. (Electron docs explicitly recommend this
|
||||
pattern.)
|
||||
- **Locator policy: semantic only.** `getByRole`, `getByLabel`,
|
||||
`getByText`, `getByPlaceholder`. No CSS selectors against minified class
|
||||
names — they rot every upstream release. No `data-testid` infrastructure
|
||||
built up front; if a specific test proves unstable, first ask upstream
|
||||
for a stable `data-testid`, only carry an `app-asar.sh` patch as a last
|
||||
resort.
|
||||
- Use Playwright auto-wait. No fixed `sleep`s anywhere in the harness.
|
||||
|
||||
### L2 — `dbus-next` + wrapped shell-outs
|
||||
|
||||
The unifying observation: most of L2 is either DBus (which `dbus-next`
|
||||
handles natively from TS) or short subprocess invocations of OS tools
|
||||
(which `child_process.exec()` handles, wrapped as a typed TS helper). No
|
||||
parallel bash test scripts; the test code reads as TS.
|
||||
|
||||
- **DBus everywhere it applies.**
|
||||
[`dbus-next`](https://github.com/dbusjs/node-dbus-next) is actively
|
||||
maintained, has TypeScript typings, and is designed for Linux desktop
|
||||
integration. Replaces `gdbus call ...` invocations:
|
||||
- Tray / SNI state queries (`org.kde.StatusNotifierWatcher`,
|
||||
`org.freedesktop.DBus`).
|
||||
- Portal availability checks (`org.freedesktop.portal.Desktop`).
|
||||
- Suspend inhibitor inspection (`org.freedesktop.login1`).
|
||||
- AT-SPI introspection where actually needed
|
||||
(`org.a11y.atspi.*`).
|
||||
- **Compositor / window-manager state via shell-out helpers.** No good
|
||||
Node bindings exist for `xprop`, `wlr-randr`, `swaymsg`, `niri msg` —
|
||||
but invoking them from `child_process.exec()` inside a TS helper is
|
||||
perfectly fine, and the test code stays unified:
|
||||
```ts
|
||||
// tools/test-harness/lib/wm.ts
|
||||
export async function listToplevels(): Promise<Toplevel[]> { ... }
|
||||
```
|
||||
Each helper is a thin typed wrapper; the test reads as TS, not
|
||||
bash-with-extra-steps.
|
||||
- **Native dialogs (T17 folder picker, etc.) via portal mocking.** The
|
||||
`org.freedesktop.portal.FileChooser` interface is just DBus. For tests
|
||||
that exercise the *integration* (does Claude make the right portal call
|
||||
and handle the result?) — which is what T17 actually tests — register
|
||||
a mock backend over `dbus-next`, intercept the call, return a canned
|
||||
path. No real dialog ever renders. This is both faster and a more
|
||||
honest unit of test than driving a real chooser.
|
||||
- **AT-SPI escape hatch.** For the rare test where portal mocking isn't
|
||||
enough (driving an *actual* GTK/Qt dialog tree), the fallback is a
|
||||
small Python [`dogtail`](https://pypi.org/project/dogtail/) script
|
||||
invoked via `child_process.exec()` — same shape as the other shell-out
|
||||
helpers, just Python on the other end. Today, T17 is the only test
|
||||
that might need this; portal mocking probably covers it. We adopt
|
||||
Python only when a specific test forces it, not speculatively.
|
||||
|
||||
### Input injection — `ydotool` now, `libei` next
|
||||
|
||||
- [`ydotool`](https://github.com/ReimuNotMoe/ydotool) goes through
|
||||
`/dev/uinput`, so it works on both X11 and Wayland. Needs root or a
|
||||
`uinput` group; not a problem inside a test VM. Invoked via the same
|
||||
`child_process` shell-out pattern — `tools/test-harness/lib/input.ts`.
|
||||
- Portal-grabbed shortcuts (T06, S11, S14) `ydotool` **cannot** trigger.
|
||||
That's a kernel-vs-compositor boundary issue, not a tool gap. Those
|
||||
tests stay manual until libei is widely available.
|
||||
- The future-correct path is
|
||||
[`libei`](https://www.phoronix.com/news/LIBEI-Emulated-Input-Wayland) +
|
||||
the `RemoteDesktop` portal via `libportal`. KDE, GNOME, and wlroots
|
||||
are all moving there. Worth a roadmap note that the shortcut tests
|
||||
have a path to automation — just not today.
|
||||
|
||||
### VM lifecycle
|
||||
|
||||
- One image-build recipe per row in `tools/test-images/${ROW}/`. Packer
|
||||
for the imperative distros (Fedora 43, Ubuntu 24.04, OmarchyOS, and
|
||||
manual-install rows like i3 / Niri); Nix flake for `Hypr-N`.
|
||||
- Rebuild nightly or per release-tag sweep — don't `apt update` /
|
||||
`dnf update` inside a test run; mirrors hiccup, tests go red for the
|
||||
wrong reason.
|
||||
- Each test gets a hermetic `XDG_CONFIG_HOME` / `CLAUDE_CONFIG_DIR`
|
||||
(S19 is already the test-isolation primitive). No shared state
|
||||
between tests.
|
||||
|
||||
## The CDP auth gate (and the runtime-attach workaround that beats it)
|
||||
|
||||
*Discovered during the first KDE-W run-through; resolved by routing
|
||||
through the in-app debugger menu's code path.*
|
||||
|
||||
The shipped `index.pre.js` contains an authenticated-CDP gate:
|
||||
|
||||
```js
|
||||
uF(process.argv) && !qL() && process.exit(1);
|
||||
```
|
||||
|
||||
`uF(argv)` matches **`--remote-debugging-port`** or
|
||||
**`--remote-debugging-pipe`** on argv. `qL()` validates an ed25519-signed
|
||||
token in `CLAUDE_CDP_AUTH` (signed payload
|
||||
`${timestamp_ms}.${base64(userDataDir)}`, 5-minute TTL) against a hardcoded
|
||||
public key. If the gate flag is on argv and a valid token isn't in env,
|
||||
the app exits with code 1 right after `frame-fix-wrapper` completes. Both
|
||||
Playwright's `_electron.launch()` and `chromium.connectOverCDP()` inject
|
||||
`--remote-debugging-port=0` and trigger the gate. The signing key is held
|
||||
upstream; we can't forge tokens.
|
||||
|
||||
**Crucially, the gate doesn't check `--inspect` or runtime SIGUSR1.** Those
|
||||
trigger the **Node inspector**, not the Chrome remote-debugging port —
|
||||
different surface. Notably, the in-app `Developer → Enable Main Process
|
||||
Debugger` menu item *also* opens the Node inspector at runtime; that
|
||||
menu's existence is the hint that this path is tolerated by upstream.
|
||||
|
||||
The harness uses this:
|
||||
|
||||
1. Spawn Electron with no debug-port flags. Gate stays asleep.
|
||||
2. Wait for the X11 window to appear (signal that the app is up).
|
||||
3. Send `SIGUSR1` to the main process pid. Same code path as the menu —
|
||||
`inspector.open()` runs at runtime and the Node inspector starts on
|
||||
port 9229.
|
||||
4. Connect a WebSocket to `http://127.0.0.1:9229/json/list[0].
|
||||
webSocketDebuggerUrl`.
|
||||
5. Use `Runtime.evaluate` to run JS in the main process. From there:
|
||||
- `webContents.getAllWebContents()` lists all live web contents
|
||||
(including `https://claude.ai/...` once it loads into the
|
||||
BrowserView).
|
||||
- `webContents.executeJavaScript(...)` drives renderer-side DOM /
|
||||
state queries.
|
||||
- Main-process mocks (e.g. `dialog.showOpenDialog = ...` for T17) are
|
||||
installed by direct assignment.
|
||||
|
||||
[`tools/test-harness/src/lib/inspector.ts`](../../tools/test-harness/src/lib/inspector.ts)
|
||||
wraps this; [`tools/test-harness/src/lib/electron.ts`](../../tools/test-harness/src/lib/electron.ts)
|
||||
exposes `app.attachInspector()` on the launched-app handle.
|
||||
|
||||
**Two implementation gotchas worth recording:**
|
||||
|
||||
- **`BrowserWindow.getAllWindows()` returns 0** because frame-fix-wrapper
|
||||
substitutes the `BrowserWindow` class and the substitution breaks the
|
||||
static registry. Use `webContents.getAllWebContents()` instead — that
|
||||
registry stays intact and includes both the shell window and the
|
||||
embedded claude.ai BrowserView.
|
||||
- **`Runtime.evaluate` with `awaitPromise: true` + `returnByValue: true`
|
||||
returns empty objects** for awaited Promise resolutions on this build's
|
||||
V8. Workaround: have the IIFE return a `JSON.stringify(value)` and
|
||||
`JSON.parse` on the caller side. `inspector.evalInMain<T>()` does this
|
||||
internally so callers don't think about it.
|
||||
|
||||
**Status of the harness today:**
|
||||
|
||||
- **L2** — fully working (DBus, xprop). T03 / T04 pass.
|
||||
- **L1 — T01** — passes via X11 window probe (no inspector needed).
|
||||
- **L1 — T17 / similar** — framework works end-to-end (verified inspector
|
||||
attach + dialog mock + webContents detection + Code-tab navigation
|
||||
click). Selector tuning to match claude.ai's actual Code-tab UI is
|
||||
ordinary iterate-as-needed work, not a blocker.
|
||||
- **No `app-asar.sh` patch needed** to neutralize the gate. The
|
||||
`dogtail`/AT-SPI escape hatch (Decision 1) is also no longer the
|
||||
fallback for L1 — it's only relevant for native dialogs that the
|
||||
inspector pattern can't reach.
|
||||
|
||||
## Notable shifts since the existing roadmap was written
|
||||
|
||||
These three changed the landscape in 2025 and the existing
|
||||
[`README.md`](./README.md) Automation roadmap section predates them:
|
||||
|
||||
1. **Electron 38+ defaults to native Wayland.** [Electron 38 release
|
||||
notes](https://www.electronjs.org/blog/electron-38-0) and the
|
||||
[Wayland tech talk](https://www.electronjs.org/blog/tech-talk-wayland)
|
||||
document this. Electron now has a Wayland CI job upstream. The project
|
||||
keeps X11 as the default backend (Decision 6) because portal coverage
|
||||
for `GlobalShortcuts` is uneven across compositors — the new tests
|
||||
characterize what works where, not what to ship by default.
|
||||
2. **Spectron is dead.** Archived Feb 2022; Playwright is the
|
||||
[official recommendation](https://www.electronjs.org/blog/spectron-deprecation-notice).
|
||||
No discussion needed about which framework — that's settled.
|
||||
3. **`libei` is real and shipping.** KWin, mutter, and wlroots have all
|
||||
moved. The shortcut-test gap (T06 / S11 / S14) is automatable in the
|
||||
medium term, not "manual forever."
|
||||
|
||||
## Anti-patterns to design against
|
||||
|
||||
Pulled from the [Playwright flaky-test
|
||||
checklist](https://testdino.com/blog/playwright-automation-checklist/),
|
||||
the [Codepipes anti-patterns
|
||||
catalogue](https://blog.codepipes.com/testing/software-testing-antipatterns.html),
|
||||
and the [TestDevLab top 5
|
||||
list](https://www.testdevlab.com/blog/5-test-automation-anti-patterns-and-how-to-avoid-them).
|
||||
Designing the harness with these in mind from day one is much cheaper than
|
||||
backing them out later:
|
||||
|
||||
| Anti-pattern | What it looks like | How to avoid in this project |
|
||||
|---|---|---|
|
||||
| Silent retry | Test passes on attempt 2; dashboard shows green; flake hidden | Log retry count to JUnit; `matrix.md` shows `✓*` for retried-pass; treat retried-pass as a Should-fix bug |
|
||||
| Async-wait by `sleep` | `sleep 5` instead of `waitFor`; ICSE 2021 found ~45% of UI flakes here | No fixed sleeps in `tools/test-harness/`. Always poll a condition (window exists, log line, DBus name owned). Lint for `\bsleep\b` and `setTimeout` with literal numbers in test code |
|
||||
| Mixing orchestration with verification | One test installs the package, launches, checks tray, asserts URL handler — five failure modes, one red cell | One test, one assertion class. Setup goes in shared fixtures, not test bodies |
|
||||
| End-to-end as the only layer | All regressions caught at full-stack UI level | Keep `scripts/patches/*.sh` independently testable; add unit-level tests on patcher logic separately from the full-app sweep |
|
||||
| Implementation-coupled selectors | `div.css-7xz92q` deep selectors against minified renderer classes | Decision 5: semantic locators only. If a selector proves unstable, first ask upstream for a stable `data-testid`; only carry an `app-asar.sh` patch as a last resort, per-test |
|
||||
| Timing-sensitive assertions | "Within 500ms after click, X appears" | Time bounds are upper-bound sanity only. Use Playwright's auto-wait with a generous `timeout`; don't fight the framework |
|
||||
| Hidden global state across tests | Test 4 fails because test 2 left `~/.config/Claude/SingletonLock` behind | Hermetic per-test `XDG_CONFIG_HOME` / `CLAUDE_CONFIG_DIR` (S19). Treat shared state as an isolation bug, not a known quirk |
|
||||
| Long-lived VM state drift | Six-month-old snapshot has stale package mirrors; tests fail with 404s | Image rebuild as code (Packer / Nix flake); rebuild nightly or per release-tag. Never `apt update` mid-test |
|
||||
| Treating skip as fail | wlroots-only test fails on KDE because it can't be skipped properly | `?` and `-` are first-class in [`matrix.md`](./matrix.md). Map JUnit `<skipped>` → `-`, `<error>` (harness broke) → `?`, only `<failure>` → `✗` |
|
||||
| Diagnostics only on failure | Test goes red; capture fires; previous green run had no baseline to diff against | Decision 7: capture `--doctor`, launcher log, screenshot **on every run**. Last 10 greens + all reds on `main` |
|
||||
| Network coupling | "Tray icon present" fails because Cloudflare hiccupped during sign-in | Tests that don't *need* network shouldn't touch it. Sign-in is one fixture; tray test runs on a pre-signed-in profile snapshot |
|
||||
|
||||
## What stays manual (for now)
|
||||
|
||||
These have no automation path that's worth the cost today, and that's
|
||||
honest to call out in the roadmap rather than pretending they'll be
|
||||
automated "soon":
|
||||
|
||||
- **T06 / S11 / S14** — global shortcut tests behind portal grabs. Path
|
||||
exists (libei + RemoteDesktop portal) but compositor-side support is
|
||||
patchy. Revisit when libei adoption broadens.
|
||||
- **T15** — sign-in browser handoff. Needs a fixture account and an
|
||||
upstream auth flow that won't necessarily welcome scripted login.
|
||||
- **T28** — scheduled task catch-up after suspend. Real wall-clock event;
|
||||
not worth simulating.
|
||||
- **Anything in `ui/` tagged "looks right"** — HiDPI sharpness, theme
|
||||
rendering, drag-feel. AT-SPI sees the tree, not the pixels.
|
||||
|
||||
T17 (folder picker) was previously in this list. Portal mocking via
|
||||
`dbus-next` moves it into L2. If real-dialog testing turns out to be
|
||||
necessary anyway, the dogtail escape hatch covers it.
|
||||
|
||||
The matrix already supports leaving these manual via the `?` / `-` /
|
||||
existing-cell semantics — no schema change needed.
|
||||
|
||||
## Suggested first vertical slice
|
||||
|
||||
The smallest end-to-end that proves every architectural decision:
|
||||
|
||||
- **One row:** KDE-W (daily-driver host, no VM startup tax).
|
||||
- **One test:** T01 — App launch.
|
||||
- **Full pipeline:** orchestrator glue → harness entry → Playwright
|
||||
`_electron.launch()` → JUnit XML → matrix-regen step → cell flips
|
||||
from `?` to `✓` automatically.
|
||||
|
||||
That single slice forces every decision out into the open: harness
|
||||
language (TS), JUnit emission, results-bundle layout, matrix-regen
|
||||
rules, diagnostic-capture format. Resist building the orchestrator
|
||||
before there's a passing test it can orchestrate. Once the slice is
|
||||
real, adding tests 2–10 is mostly mechanical.
|
||||
|
||||
After T01: the next sensible additions are T03 (tray — exercises
|
||||
`dbus-next` end-to-end), T04 (window decorations — exercises the
|
||||
shell-out helper pattern), and T17 (folder picker — exercises portal
|
||||
mocking). Those four runners cover every distinct shape of TS code in
|
||||
the harness; everything else after them is a recombination.
|
||||
|
||||
## Still open
|
||||
|
||||
Most of the framing decisions are settled in the [Decisions](#decisions)
|
||||
table. What remains:
|
||||
|
||||
1. **Owner assignments per row.** [`MEMORY.md`](https://github.com/aaddrick/claude-desktop-debian/blob/main/.claude/projects/-home-aaddrick-source-claude-desktop-debian/memory/MEMORY.md)
|
||||
notes cowork → @RayCharlizard, nix → @typedrat. Hypr-N row is the
|
||||
natural fit for @typedrat once the Nix flake exists. The other eight
|
||||
rows: aaddrick by default, but worth asking the contributor base in a
|
||||
discussion thread.
|
||||
2. **AT-SPI escape-hatch trigger.** Decision 1 punts on Python until a
|
||||
specific test forces it. T17 is the only candidate today, and portal
|
||||
mocking probably covers it. If T17 actually needs real-dialog
|
||||
automation, that's the first reopen.
|
||||
3. **Selector rot rate.** Decision 5 starts with semantic locators and
|
||||
measures. After ~20 tests on the renderer, revisit whether
|
||||
`getByRole`/`getByText` is holding up or whether per-test
|
||||
`data-testid` patches are warranted. No prediction; this is a
|
||||
measure-and-decide.
|
||||
4. **CI execution model.** Decision 4 punts on this entirely until the
|
||||
harness has signal on which tests are stable. Reopen after the first
|
||||
~20 tests have run from the dev box for a few weeks.
|
||||
5. **Smoke-set Wayland-default test wording.** Decision 6 calls for a
|
||||
Smoke test asserting X11/XWayland selection on each row, plus
|
||||
per-row Should tests for Wayland characterization. The exact T-IDs
|
||||
and case-file homes for those tests need to be drafted next time
|
||||
`cases/` is touched.
|
||||
|
||||
## Sources
|
||||
|
||||
Background reading the recommendations draw on. Linked here so the
|
||||
calls have receipts:
|
||||
|
||||
### Electron testing & Playwright
|
||||
- [Electron — Automated Testing](https://www.electronjs.org/docs/latest/tutorial/automated-testing) — official tutorial, recommends Playwright
|
||||
- [Electron — Spectron Deprecation Notice](https://www.electronjs.org/blog/spectron-deprecation-notice) — Feb 2022 archive
|
||||
- [Playwright — Electron class](https://playwright.dev/docs/api/class-electron)
|
||||
- [Playwright — ElectronApplication class](https://playwright.dev/docs/api/class-electronapplication)
|
||||
- [Testing Electron apps with Playwright and GitHub Actions (Simon Willison)](https://til.simonwillison.net/electron/testing-electron-playwright)
|
||||
- [`spaceagetv/electron-playwright-example`](https://github.com/spaceagetv/electron-playwright-example) — multi-window Playwright + Electron example
|
||||
|
||||
### DBus / TypeScript
|
||||
- [`dbus-next` — actively-maintained Node DBus library with TS typings](https://github.com/dbusjs/node-dbus-next)
|
||||
- [`dbus-next` on npm](https://www.npmjs.com/package/dbus-next)
|
||||
|
||||
### Wayland / X11 / input injection
|
||||
- [Electron — Tech Talk: How Electron went Wayland-native](https://www.electronjs.org/blog/tech-talk-wayland)
|
||||
- [Electron 38.0.0 release notes](https://www.electronjs.org/blog/electron-38-0)
|
||||
- [PR #33355: fix calling X11 functions under Wayland](https://github.com/electron/electron/pull/33355)
|
||||
- [LIBEI — Phoronix overview](https://www.phoronix.com/news/LIBEI-Emulated-Input-Wayland)
|
||||
- [libei + RemoteDesktop portal — RustDesk discussion](https://github.com/rustdesk/rustdesk/discussions/4515)
|
||||
- [`ydotool` README](https://github.com/ReimuNotMoe/ydotool)
|
||||
- [`kwin-mcp` — KDE Plasma 6 Wayland automation tools](https://github.com/isac322/kwin-mcp)
|
||||
|
||||
### Portals / AT-SPI
|
||||
- [XDG Desktop Portal — main repo](https://github.com/flatpak/xdg-desktop-portal)
|
||||
- [`org.freedesktop.portal.FileChooser` interface XML](https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.FileChooser.xml)
|
||||
- [File Chooser portal documentation](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html)
|
||||
- [`dogtail` on PyPI](https://pypi.org/project/dogtail/) — fallback only
|
||||
- [Automation through Accessibility — Fedora Magazine](https://fedoramagazine.org/automation-through-accessibility/)
|
||||
|
||||
### Anti-patterns / flaky tests
|
||||
- [Playwright automation checklist to reduce flaky tests (TestDino)](https://testdino.com/blog/playwright-automation-checklist/)
|
||||
- [Flaky Tests: The Complete Guide to Detection & Prevention (TestDino)](https://testdino.com/blog/flaky-tests/)
|
||||
- [5 Test Automation Anti-Patterns (TestDevLab)](https://www.testdevlab.com/blog/5-test-automation-anti-patterns-and-how-to-avoid-them)
|
||||
- [Software Testing Anti-patterns (Codepipes)](https://blog.codepipes.com/testing/software-testing-antipatterns.html)
|
||||
|
||||
### JUnit XML reporting
|
||||
- [`junit-to-md`](https://github.com/davidahouse/junit-to-md)
|
||||
- [Test Summary GitHub Action](https://github.com/marketplace/actions/junit-test-dashboard)
|
||||
- [Test Reporter](https://github.com/marketplace/actions/test-reporter)
|
||||
|
||||
### CI / VM matrix
|
||||
- [Transient — QEMU CI wrapper](https://www.starlab.io/blog/simple-painless-application-testing-on-virtualized-hardwarenbsp)
|
||||
- [`cirruslabs/tart` — VMs for CI automation](https://github.com/cirruslabs/tart)
|
||||
|
||||
---
|
||||
|
||||
*Once the first vertical slice (KDE-W + T01) ships, the relevant pieces of
|
||||
this file fold into [`README.md`](./README.md) (Automation roadmap) and
|
||||
[`runbook.md`](./runbook.md) (the harness invocation). Until then: working
|
||||
notes that have crossed from brainstorm to plan.*
|
||||
@@ -0,0 +1,94 @@
|
||||
# Functional Test Cases
|
||||
|
||||
Test specifications grouped by feature surface. For live status, see [`../matrix.md`](../matrix.md). For sweep workflow, see [`../runbook.md`](../runbook.md).
|
||||
|
||||
## Files
|
||||
|
||||
| File | Surfaces covered | Tests |
|
||||
|------|------------------|-------|
|
||||
| [`launch.md`](./launch.md) | App startup, doctor, package detection, multi-instance | T01, T02, T13, T14 |
|
||||
| [`tray-and-window-chrome.md`](./tray-and-window-chrome.md) | Tray icon, window decorations, hybrid topbar, hide-to-tray | T03, T04, T07, T08, S08, S13 |
|
||||
| [`shortcuts-and-input.md`](./shortcuts-and-input.md) | URL handler, Quick Entry, global shortcuts | T05, T06, S06, S07, S09, S10, S11, S12, S14, S29, S30, S31, S32, S33, S34, S35, S36, S37 |
|
||||
| [`code-tab-foundations.md`](./code-tab-foundations.md) | Sign-in, Code tab load, folder picker, drag-drop, terminal, file pane | T15, T16, T17, T18, T19, T20 |
|
||||
| [`code-tab-workflow.md`](./code-tab-workflow.md) | Preview, PR monitor, worktrees, auto-archive, side chat, slash menu | T21, T22, T29, T30, T31, T32 |
|
||||
| [`code-tab-handoff.md`](./code-tab-handoff.md) | Notifications, external editor, file manager, connector OAuth, IDE handoff | T23, T24, T25, T34, T38, T39 |
|
||||
| [`routines.md`](./routines.md) | Scheduled tasks, catch-up runs, suspend inhibit, config dir | T26, T27, T28, S19, S20, S21 |
|
||||
| [`extensibility.md`](./extensibility.md) | Plugins, MCP, hooks, CLAUDE.md memory, worktree storage | T11, T33, T35, T36, T37, S27, S28 |
|
||||
| [`distribution.md`](./distribution.md) | DEB, RPM, AppImage, dependency pulls, auto-update | S01, S02, S03, S04, S05, S15, S16, S26 |
|
||||
| [`platform-integration.md`](./platform-integration.md) | Autostart, Cowork, WebGL, PATH inheritance, Computer Use, Dispatch | T09, T10, T12, S17, S18, S22, S23, S24, S25 |
|
||||
|
||||
## Standard test body
|
||||
|
||||
Every test in this directory follows this structure:
|
||||
|
||||
```markdown
|
||||
### T## — Title
|
||||
|
||||
**Severity:** Smoke | Critical | Should | Could
|
||||
**Surface:** human-readable surface tag (e.g. "Code tab → Environment")
|
||||
**Applies to:** All | <subset of rows>
|
||||
**Issues:** linked issue/PR list, or `—`
|
||||
|
||||
**Steps:**
|
||||
1. ...
|
||||
2. ...
|
||||
|
||||
**Expected:** what should happen.
|
||||
|
||||
**Diagnostics on failure:** which captures to attach when filing. See [`../runbook.md#diagnostic-capture`](../runbook.md#diagnostic-capture).
|
||||
|
||||
**References:** docs links, learnings, related issues.
|
||||
|
||||
**Code anchors:** `<file>:<line>` pointers to the upstream code or
|
||||
wrapper script that backs the load-bearing claim above. Added during
|
||||
the grounding sweep — see "Anchor scope" for guidance on where
|
||||
anchors can and can't land.
|
||||
|
||||
**Inventory anchor:** (optional) `<element-id>` from
|
||||
[`../ui-inventory.json`](../ui-inventory.json) — only if the surface
|
||||
shows up in the v7 walker's idle capture. For surfaces inside modals
|
||||
or popups, append a sentence noting which click-chain opens them so
|
||||
the next inventory regeneration can grab them.
|
||||
```
|
||||
|
||||
The Steps and Diagnostics fields are written so they can later become
|
||||
script entry points without a rewrite.
|
||||
|
||||
### Anchor scope
|
||||
|
||||
Where the load-bearing claim lives determines where the anchor goes:
|
||||
|
||||
- **Upstream code** — any file under
|
||||
`build-reference/app-extracted/.vite/build/` (most often `index.js`,
|
||||
the main process). Use `index.js:N` style anchors.
|
||||
- **Our wrapper code** — `scripts/launcher-common.sh`, `scripts/doctor.sh`,
|
||||
`scripts/patches/*.sh`, `scripts/frame-fix-wrapper.js`,
|
||||
`scripts/wco-shim.js`. Use `<repo-relative-path>:N` style anchors.
|
||||
- **Server-rendered (claude.ai SPA)** — anchorable only via the v7
|
||||
walker inventory (`docs/testing/ui-inventory.json`) or a runtime
|
||||
capture from `tools/test-harness/grounding-probe.ts`. Idle-state
|
||||
inventory misses contextual surfaces (modals, popups, slash menus,
|
||||
context menus, side panels) — note that explicitly.
|
||||
- **Upstream `claude` CLI binary** — out of scope for this matrix
|
||||
(e.g. T39 `/desktop` is a CLI slash-command, not in the Electron
|
||||
asar). Mark as Ambiguous and link to a separate CLI matrix if one
|
||||
exists.
|
||||
|
||||
If a claim spans multiple scopes (a wrapper script triggering
|
||||
upstream behavior, e.g. T01's launcher-log + main-window-opens),
|
||||
list all the anchors. The whole point is making the next sweep
|
||||
faster — over-anchoring is fine, missing anchors is not.
|
||||
|
||||
### Drift markers
|
||||
|
||||
When a sweep finds upstream behavior no longer matches the case:
|
||||
|
||||
- **Edited Steps/Expected** — fix the case in place, mention what
|
||||
changed in the commit message. The case is the spec.
|
||||
- **Missing in build X.Y.Z** — prepend a blockquote under the test
|
||||
heading: `> **⚠ Missing in build 1.5354.0** — <one-line note>.
|
||||
Re-verify after next upstream bump.` Use when the feature isn't
|
||||
in the build at all (deprecated, behind unset flag, never shipped).
|
||||
- **Ambiguous** — don't edit; flag in the sweep report. Use when
|
||||
the load-bearing claim could be one of several candidate code
|
||||
paths and static analysis can't disambiguate.
|
||||
@@ -0,0 +1,197 @@
|
||||
# Code Tab — Foundations
|
||||
|
||||
Tests covering Code-tab availability on Linux (officially unsupported per upstream docs), sign-in flow, folder picker, drag-and-drop, and the basic editing surfaces (terminal, file pane). See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T15 — Sign-in completes in the embedded webview
|
||||
|
||||
> **Drift in build 1.5354.0** — Sign-in is an in-app `mainView.webContents.loadURL` flow, not an `xdg-open` browser handoff. Claude.ai/login renders inside the embedded BrowserView; the resulting `sessionKey` cookie is then exchanged at `${apiHost}/v1/oauth/${org}/authorize` with redirect URI `https://claude.ai/desktop/callback`. No system browser is involved.
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Auth / embedded webview
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch a fresh app instance (signed-out state).
|
||||
2. Click **Sign in**. Observe claude.ai/login rendering inside the app.
|
||||
3. Authenticate. Observe the in-app navigation completing back to the
|
||||
workspace.
|
||||
|
||||
**Expected:** Sign-in stays inside the embedded webview (`will-navigate`
|
||||
handler `Ihr` keeps `/login/` paths in-app). After auth the
|
||||
`sessionKey` cookie is captured and silently exchanged for an OAuth
|
||||
token via the `desktop/callback` redirect. Account dropdown populates;
|
||||
no auth banner remains.
|
||||
|
||||
**Diagnostics on failure:** DevTools console for the `mainView`
|
||||
BrowserView, network captures of the `/v1/oauth/{org}/authorize` and
|
||||
`/v1/oauth/token` calls, launcher log, cookie jar inspection
|
||||
(`sessionKey` on `.claude.ai`).
|
||||
|
||||
**References:** [Code tab auth troubleshooting](https://code.claude.com/docs/en/desktop#403-or-authentication-errors-in-the-code-tab)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:141996` — desktop
|
||||
OAuth redirect URI `https://claude.ai/desktop/callback`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:142431` — POST to
|
||||
`${apiHost}/v1/oauth/${org}/authorize` with `Bearer ${sessionKey}`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:216565` — `Ihr`
|
||||
treats `/login/` paths as in-app (not external)
|
||||
- `build-reference/app-extracted/.vite/build/index.js:141316` —
|
||||
`mainView.webContents.loadURL(...)` drives the embedded sign-in
|
||||
|
||||
## T16 — Code tab loads
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Code tab — top-level UI
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. After sign-in, click the **Code** tab at the top center.
|
||||
2. Wait a few seconds.
|
||||
|
||||
**Expected:** Code tab renders the session UI (sidebar, prompt area, environment dropdown). Per upstream docs the Code tab is "not supported" on Linux — the patched build under this project should render the UI normally or surface a clear, actionable message. Not a blank screen, infinite spinner, or `Error 403: Forbidden`.
|
||||
|
||||
**Diagnostics on failure:** Screenshot, DevTools console, network captures (auth/feature-flag responses), launcher log, the active patch set in `scripts/patches/`.
|
||||
|
||||
**References:** [Use Claude Code Desktop](https://code.claude.com/docs/en/desktop), [Get started with the desktop app](https://code.claude.com/docs/en/desktop-quickstart)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:525066` —
|
||||
`sidebarMode === "code"` rewrites the BrowserView path to `/epitaxy`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:496066` — Code
|
||||
deeplinks (`claude://code?...`) navigate to `/epitaxy?...`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:105273` — `IHi`
|
||||
recognises `/epitaxy` and `/epitaxy/...` as the Code-tab path
|
||||
- `build-reference/app-extracted/.vite/build/index.js:105346` —
|
||||
`sidebarMode` enum contains `"code"`
|
||||
|
||||
**Inventory anchor:** `…tablist.tab-by-name.code` (role `tab`, label
|
||||
`Code`) — confirms the Code tab is reachable from the new-chat tablist
|
||||
in the captured idle state.
|
||||
|
||||
## T17 — Folder picker opens
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Code tab → Environment selection
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
**Runner:** [`tools/test-harness/src/runners/T17_folder_picker.spec.ts`](../../../tools/test-harness/src/runners/T17_folder_picker.spec.ts) — runtime-attach via SIGUSR1 + main-process `dialog.showOpenDialog` mock + `webContents.executeJavaScript` to drive the renderer. Click chain to reach the folder-picker button awaits selector tuning
|
||||
|
||||
**Steps:**
|
||||
1. In the Code tab, click the environment pill → **Local** → **Select folder**.
|
||||
2. Choose a project directory.
|
||||
|
||||
**Expected:** Native file chooser opens. On Wayland sessions the chooser is `xdg-desktop-portal`-backed (verify with `busctl --user tree org.freedesktop.portal.Desktop`). On X11 sessions the GTK/Qt native picker fires. Selected path appears in the env pill.
|
||||
|
||||
**Diagnostics on failure:** `systemctl --user status xdg-desktop-portal`, `XDG_SESSION_TYPE`, the portal backend in use (`xdg-desktop-portal-kde`, `xdg-desktop-portal-gnome`, `xdg-desktop-portal-wlr`), launcher log.
|
||||
|
||||
**References:** [Local sessions](https://code.claude.com/docs/en/desktop#local-sessions)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:66403` — IPC
|
||||
channel `claude.web_FileSystem_browseFolder` (renderer → main)
|
||||
- `build-reference/app-extracted/.vite/build/index.js:509188` —
|
||||
`browseFolder` impl calls `dialog.showOpenDialog` with
|
||||
`properties: ["openDirectory", "createDirectory"]`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:450534` —
|
||||
`grantViaPicker` (Operon host-access folder grant) uses the same
|
||||
`["openDirectory"]` shape
|
||||
- `tools/test-harness/src/lib/claudeai.ts:122` — `installOpenDialogMock`
|
||||
intercepts both `(opts)` and `(window, opts)` arities, matching the
|
||||
call sites at index.js:509196 and :450534
|
||||
|
||||
**Inventory anchor:** `root.main.region.button-by-name.select-folder`
|
||||
(role `button`, label `Select folder…`) — the persistent button the
|
||||
T17 runner clicks before the dialog mock fires.
|
||||
|
||||
## T18 — Drag-and-drop files into prompt
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Code tab → Prompt area
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Open a Code-tab session.
|
||||
2. From the system file manager, drag one or more files into the prompt area.
|
||||
3. Repeat with multiple files at once.
|
||||
|
||||
**Expected:** Files attach to the prompt. The renderer resolves dropped
|
||||
`File` objects to absolute paths via the preload-bridged
|
||||
`claudeAppSettings.filePickers.getPathForFile` (Electron's
|
||||
`webUtils.getPathForFile`). Multi-file drops attach each file. Works on
|
||||
both Wayland and X11.
|
||||
|
||||
**Diagnostics on failure:** Screen recording, `wl-paste --list-types` (Wayland) or `xclip -selection clipboard -t TARGETS -o` (X11) during drag, DevTools console, launcher log.
|
||||
|
||||
**References:** [Add files and context](https://code.claude.com/docs/en/desktop#add-files-and-context-to-prompts)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/mainView.js:9267` —
|
||||
`filePickers.getPathForFile` wraps `webUtils.getPathForFile`
|
||||
- `build-reference/app-extracted/.vite/build/mainView.js:9552` —
|
||||
exposed to the renderer as `window.claudeAppSettings`
|
||||
|
||||
## T19 — Integrated terminal
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Code tab → Terminal pane
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, press `` Ctrl+` `` (or open via the Views menu).
|
||||
2. Confirm the terminal opens in the session's working directory.
|
||||
3. Run `git status`, `npm --version`, `gh auth status`.
|
||||
|
||||
**Expected:** Terminal pane opens in the session's working directory, inherits the same `PATH` Claude sees. Standard commands run cleanly. Terminal pane is local-session-only per docs.
|
||||
|
||||
**Diagnostics on failure:** Terminal pane content, `echo $PATH` from inside the pane, `pwd`, the shell binary in use, launcher log.
|
||||
|
||||
**References:** [Run commands in the terminal](https://code.claude.com/docs/en/desktop#run-commands-in-the-terminal)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:69135` — IPC
|
||||
channel `claude.web_LocalSessions_startShellPty` (also
|
||||
`resizeShellPty`, `writeShellPty` at :69184, :69210)
|
||||
- `build-reference/app-extracted/.vite/build/index.js:486438` —
|
||||
`startShellPty` body: spawns `node-pty` in
|
||||
`n.worktreePath ?? n.cwd` with `TERM=xterm-256color`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:486463` —
|
||||
`node-pty` dynamic import (optional dep, `package.json` line 100)
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259306` —
|
||||
`shell-path-worker/shellPathWorker.js` resolves the user's interactive
|
||||
PATH; `FX()` (line 259311) returns it for the spawned PTY env
|
||||
|
||||
## T20 — File pane opens and saves
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Code tab → File pane
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, click a file path in chat or diff to open it in the file pane.
|
||||
2. Make a small edit. Click **Save**.
|
||||
3. Modify the file externally (e.g. `echo >> file`). Re-edit in the pane. Observe the on-disk-changed warning.
|
||||
|
||||
**Expected:** File opens in the editor pane. Edits write back to disk on Save. If the file changed on disk since opening, the pane shows the on-disk-changed warning and offers override or discard. (The conflict check is sha256-based, not mtime-based — `writeSessionFile` reads the current bytes, hashes them, and rejects with `Conflict` if the renderer-supplied `expectedHash` doesn't match.)
|
||||
|
||||
**Diagnostics on failure:** `sha256sum <file>` output (and stat mtime for cross-checking), launcher log, DevTools console, screen recording of the warning state.
|
||||
|
||||
**References:** [Open and edit files](https://code.claude.com/docs/en/desktop#open-and-edit-files)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:68922` — IPC
|
||||
channel `claude.web_LocalSessions_readSessionFile`
|
||||
- `build-reference/app-extracted/.vite/build/index.js:69003` — IPC
|
||||
channel `claude.web_LocalSessions_writeSessionFile` with
|
||||
`expectedHash` argument at position 3
|
||||
- `build-reference/app-extracted/.vite/build/index.js:492874` —
|
||||
`readSessionFile` impl
|
||||
- `build-reference/app-extracted/.vite/build/index.js:492954` —
|
||||
`writeSessionFile` impl: sha256-hashes current on-disk bytes,
|
||||
returns `{ status: nW.Conflict, currentHash }` when `expectedHash`
|
||||
mismatches
|
||||
@@ -0,0 +1,163 @@
|
||||
# Code Tab — Handoffs to Other Apps
|
||||
|
||||
Tests covering desktop notifications, "Open in" external editor, "Show in Files" file manager, connector OAuth round-trips, IDE handoff, and graceful failure of the macOS/Windows-only `/desktop` CLI command. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T23 — Desktop notifications fire
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Notifications (libnotify / XDG Notifications)
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Trigger each notification source: scheduled-task fire ([T27](./routines.md#t27--scheduled-task-fires-and-notifies)), CI completion ([T22](./code-tab-workflow.md#t22--pr-monitoring-via-gh)), Dispatch handoff ([S24](./platform-integration.md#s24--dispatch-spawned-code-session-appears-with-badge-and-notification)).
|
||||
2. Observe each notification appears.
|
||||
3. Click each — confirm it focuses the relevant session.
|
||||
|
||||
**Expected:** Notifications appear in the active DE's notification area (Plasma's notification daemon, Mako on wlroots, gnome-shell, etc.) and are clickable to focus the relevant session.
|
||||
|
||||
**Diagnostics on failure:** `gdbus call --session --dest=org.freedesktop.Notifications --object-path=/org/freedesktop/Notifications --method=org.freedesktop.DBus.Introspectable.Introspect`, `notify-send "test"` (sanity check daemon), launcher log, DE-specific notification logs.
|
||||
|
||||
**References:** [Scheduled tasks](https://code.claude.com/docs/en/desktop-scheduled-tasks), [Monitor pull request status](https://code.claude.com/docs/en/desktop#monitor-pull-request-status)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:494456` (`new hA.Notification(r)` — backed by Electron's libnotify on Linux); `:495110` (`showNotification(title, body, tag, navigateTo)` dispatches Swift on macOS, Electron elsewhere); `:511174`, `:512738` (cu-lock / tool-permission notifications wire a click callback that navigates to `/local_sessions/{sessionId}` to focus the session).
|
||||
|
||||
## T24 — Open in external editor
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Right-click → Open in
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Install at least one of: VS Code, Cursor, Zed, Windsurf (any install method —
|
||||
flatpak, AppImage, distro package). Xcode is darwin-only and absent on Linux.
|
||||
2. In the Code tab, right-click a file path → **Open in** → choose the editor.
|
||||
3. Confirm the editor opens at that file.
|
||||
|
||||
**Expected:** Right-click → **Open in** launches the chosen editor with the file
|
||||
path. Editor is invoked by URL scheme (`vscode://file/<path>`,
|
||||
`cursor://file/<path>`, `zed://file/<path>`, `windsurf://file/<path>`) via
|
||||
`shell.openExternal`, which delegates to `xdg-open`'s
|
||||
`x-scheme-handler/<editor>` resolution rather than hard-coded paths.
|
||||
|
||||
**Diagnostics on failure:** `xdg-mime query default x-scheme-handler/vscode` (or
|
||||
`cursor`/`zed`/`windsurf`), `desktop-file-validate` on the editor's `.desktop`
|
||||
file, `xdg-open vscode://file/<path>` from terminal (sanity check), launcher
|
||||
log.
|
||||
|
||||
**References:** [Open files in other apps](https://code.claude.com/docs/en/desktop#open-files-in-other-apps)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:59076`
|
||||
(editor enum: VSCode, Cursor, Zed, Windsurf, Xcode); `:463902` (`Mtt`
|
||||
registry — `vscode://`, `cursor://`, `zed://`, `windsurf://`, `xcode://` with
|
||||
darwin-only flag on Xcode); `:463956` (`getInstalledEditors` probes via
|
||||
`app.getApplicationInfoForProtocol`); `:464011`
|
||||
(`shell.openExternal('<scheme>://file/<encoded-path>:<line>')` — path is
|
||||
URL-encoded but `/` separators are preserved); `:68816` IPC handler
|
||||
`LocalSessions.openInEditor(path, editor, sshConfig, line)`.
|
||||
|
||||
## T25 — Show in Files / file manager
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Right-click → Show in Files
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In the Code tab, right-click a file path → "Show in Files" (Linux equivalent of macOS "Show in Finder" / Windows "Show in Explorer").
|
||||
2. Confirm the system file manager opens with the containing folder selected.
|
||||
|
||||
**Expected:** System file manager (Nautilus on GNOME, Dolphin on KDE, Thunar on Xfce, etc.) opens with the file pre-selected. Resolution respects `xdg-mime` defaults.
|
||||
|
||||
**Diagnostics on failure:** `xdg-mime query default inode/directory`, `xdg-open <dir>` from terminal, the menu label rendered (was it Linux-specific or stuck on "Show in Finder"?), launcher log.
|
||||
|
||||
**References:** [Open files in other apps](https://code.claude.com/docs/en/desktop#open-files-in-other-apps)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:66652` IPC
|
||||
handler `FileSystem.showInFolder(path)`; `:509431` impl thin-wraps
|
||||
`hA.shell.showItemInFolder(Tc(path))`. Electron's `showItemInFolder` on Linux
|
||||
falls back to `xdg-open` on the parent directory when no DBus FileManager1
|
||||
service is present, so the file is rarely pre-selected on minimal DEs — only
|
||||
the parent folder opens.
|
||||
|
||||
## T34 — Connector OAuth round-trip
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Connectors → OAuth handoff
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, click **+** → **Connectors** → choose a service (Slack, GitHub, Linear, Notion, Google Calendar).
|
||||
2. Step through the OAuth flow in the system browser.
|
||||
3. Return to Claude Desktop and verify the connector appears in **Settings → Connectors**.
|
||||
4. Use the connector in a prompt (e.g. "list my Slack channels").
|
||||
|
||||
**Expected:** Adding a connector launches the browser via `xdg-open`, OAuth callback hands control back to Claude Desktop, connector appears in Settings, and is usable in subsequent prompts.
|
||||
|
||||
**Diagnostics on failure:** `xdg-mime query default x-scheme-handler/https`, the callback URL scheme, network captures of OAuth redirect, launcher log, DevTools console.
|
||||
|
||||
**References:** [Connect external tools](https://code.claude.com/docs/en/desktop#connect-external-tools), [Connectors for everyday life](https://claude.com/blog/connectors-for-everyday-life)
|
||||
|
||||
**Code anchors:**
|
||||
`build-reference/app-extracted/.vite/build/index.js:524819`
|
||||
(`hA.app.setAsDefaultProtocolClient("claude")` — registers the `claude://`
|
||||
deep-link scheme used by the OAuth callback); `:525026` mainWindow
|
||||
`setWindowOpenHandler` routes external URLs through `MAA(url)` →
|
||||
`:525102`–`:525135` (only `http:`/`https:`/`mailto:`/`tel:`/`sms:`/
|
||||
`ms-(excel|powerpoint|word):` are forwarded to system handlers; everything
|
||||
else is dropped); `:136233` `$a(url)` thin-wraps `hA.shell.openExternal(url)`
|
||||
(this is the single egress point for browser handoff); `:159634`
|
||||
`mcpSubmitOAuthCallbackUrl(serverName, callbackUrl)` and `:159651`
|
||||
`claudeOAuthCallback(authorizationCode, state)` — IPC bridges that consume
|
||||
the deep-link callback. See [`docs/learnings/plugin-install.md`](../../learnings/plugin-install.md)
|
||||
for orgId/sessionKey cookie chain that gates connector listing.
|
||||
|
||||
## T38 — Continue in IDE
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Continue in menu
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, click the IDE icon (bottom right of session toolbar) → **Continue in** → choose an IDE.
|
||||
2. Confirm the IDE opens at the working directory.
|
||||
|
||||
**Expected:** Selected IDE opens the project at the current working directory. Resolution via `xdg-open` / `.desktop` files.
|
||||
|
||||
**Diagnostics on failure:** `xdg-open <project-dir>` sanity check, `xdg-mime query default x-scheme-handler/vscode` (or matching scheme for the chosen IDE), launcher log, the IDE's `.desktop` file.
|
||||
|
||||
**References:** [Continue in another surface](https://code.claude.com/docs/en/desktop#continue-in-another-surface)
|
||||
|
||||
**Code anchors:** Same IPC surface as [T24](#t24--open-in-external-editor) —
|
||||
`build-reference/app-extracted/.vite/build/index.js:68816`
|
||||
(`LocalSessions.openInEditor(path, editor, sshConfig, line)` accepts a
|
||||
directory path the same way as a file path); `:463902` editor registry;
|
||||
`:464011` `shell.openExternal('<scheme>://file/<cwd>')`. The "Continue in"
|
||||
chooser UI is rendered server-side by claude.ai and not present in the local
|
||||
asar — only the IPC bridge can be code-anchored.
|
||||
|
||||
## T39 — `/desktop` CLI handoff (graceful N/A)
|
||||
|
||||
> **Note** — This test exercises the upstream `claude` CLI binary, not the
|
||||
> Electron app. The CLI ships separately from this packaging (out of
|
||||
> `build-reference/`), so no anchor in `app-extracted/.vite/build/` exists for
|
||||
> the slash-command handler. Re-verify behaviour against the CLI binary that
|
||||
> ships with the upstream version under test (currently 1.5354.0).
|
||||
|
||||
**Severity:** Could
|
||||
**Surface:** CLI `/desktop` command
|
||||
**Applies to:** All rows (Linux equally)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a CLI session, run `/desktop`.
|
||||
2. Inspect exit code and output.
|
||||
|
||||
**Expected:** `/desktop` is documented as macOS/Windows-only. On Linux it must fail gracefully — print a clear "not supported on Linux" message and exit cleanly. No partial state transition, no panic, no corrupted session file.
|
||||
|
||||
**Diagnostics on failure:** Full CLI output, exit code, the session file before/after (`~/.claude/sessions/...`), strace if the CLI hangs.
|
||||
|
||||
**References:** [Coming from the CLI](https://code.claude.com/docs/en/desktop#coming-from-the-cli)
|
||||
@@ -0,0 +1,151 @@
|
||||
# Code Tab — Workflow Surfaces
|
||||
|
||||
Tests covering the dev-server preview pane, PR monitoring, worktree isolation, auto-archive, side chat, and the slash command menu. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T21 — Dev server preview pane
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Preview pane
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, ensure `.claude/launch.json` is configured (or let auto-detect populate it).
|
||||
2. Click **Preview** dropdown → **Start**.
|
||||
3. Interact with the embedded browser. Verify auto-verify takes screenshots.
|
||||
4. Stop the server from the dropdown.
|
||||
|
||||
**Expected:** Configured dev server starts. Embedded browser renders the running app. Auto-verify takes screenshots and inspects DOM. Stopping from the dropdown actually stops the process.
|
||||
|
||||
**Diagnostics on failure:** `lsof -i :<port>` to see the server, screenshot of preview pane state, `.claude/launch.json` content, launcher log, DevTools console.
|
||||
|
||||
**References:** [Preview your app](https://code.claude.com/docs/en/desktop#preview-your-app)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:262175` — `Pae = "Claude Preview"` + `preview_*` MCP tool table (`preview_start`, `preview_stop`, `preview_list`, `preview_screenshot`, `preview_snapshot`, `preview_inspect`, `preview_click`, `preview_fill`, `preview_eval`, `preview_network`, `preview_resize`).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259604` — `setAutoVerify()` and `parseLaunchJson()` (reads `.claude/launch.json`, honours `autoVerify` flag default-on).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:260015` — `capturePage()` / `captureViaCDP()` drive `preview_screenshot` against the embedded preview WebContents.
|
||||
|
||||
## T22 — PR monitoring via `gh`
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Code tab → CI status bar
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Ensure `gh` is installed and authenticated (`gh auth status`).
|
||||
2. In a Code-tab session, ask Claude to open a PR for a small change.
|
||||
3. Observe the CI status bar. Toggle **Auto-fix** and **Auto-merge**.
|
||||
4. Run a separate test on a row where `gh` is **not** installed — confirm the missing-`gh` prompt appears the first time a PR action is taken.
|
||||
|
||||
**Expected:** With `gh` present and authenticated, CI status bar surfaces in the session toolbar. Auto-fix and Auto-merge toggles work (auto-merge requires the corresponding GitHub repo setting). If `gh` is missing, the app surfaces a prompt directing the user to https://cli.github.com (auto-install via `installGh` only runs on macOS/brew; Linux returns an error string with the install URL).
|
||||
|
||||
**Diagnostics on failure:** `gh auth status`, `which gh`, launcher log, DevTools console, screenshot of status bar, the GitHub repo's "Allow auto-merge" setting.
|
||||
|
||||
**References:** [Monitor pull request status](https://code.claude.com/docs/en/desktop#monitor-pull-request-status)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:464281` — `GitHubPrManager` (`prStateCache`, `prChecksCache`); `getPrChecks` at line 464964 fans out to `gh pr view`.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:464368` — `"gh CLI not found in PATH"` throw site that backs the missing-`gh` prompt.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:464480` — `installGh()`: macOS-only `brew install gh`; Linux/Windows return error pointing to https://cli.github.com.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:465019` — `autoMergeRequest { enabledAt }` GraphQL fragment; `enableAutoMerge` / `disableAutoMerge` at lines 465531 / 465556.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:534033` — `AutoFixEngine.handleSessionEvent` toggles on `autoFixEnabled` per session.
|
||||
|
||||
## T29 — Worktree isolation
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Code tab → Sidebar (parallel sessions)
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session against a Git project, open two new sessions in parallel via **+ New session**.
|
||||
2. Make different edits in each session.
|
||||
3. Confirm `<project-root>/.claude/worktrees/<branch>` exists for each.
|
||||
4. Archive one session via the sidebar archive icon.
|
||||
|
||||
**Expected:** Each session creates an isolated worktree at `<project-root>/.claude/worktrees/<branch>` (or the dir configured in Settings → Claude Code → "Worktree location"). Edits in one session do not appear in another until committed. Archiving removes the worktree.
|
||||
|
||||
**Diagnostics on failure:** `git worktree list` from project root, `ls -la <project-root>/.claude/worktrees/`, launcher log.
|
||||
|
||||
**References:** [Work in parallel with sessions](https://code.claude.com/docs/en/desktop#work-in-parallel-with-sessions)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:462835` — `getWorktreeParentDir()`: returns `<baseRepo>/.claude/worktrees`, or `<chillingSlothLocation.customPath>/<basename>` when overridden in Settings.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:462843` — `createWorktree()`: runs `git worktree add` with `core.longpaths=true` under the parent dir.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:463290` — `git worktree remove --force` invoked on archive (cleanup path).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:55231` — `chillingSlothLocation: "default"` settings key (Settings → "Worktree location").
|
||||
|
||||
## T30 — Auto-archive on PR merge
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Sidebar
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In Settings → Claude Code, enable **Auto-archive on PR close** (`ccAutoArchiveOnPrClose`).
|
||||
2. Open a PR from a local session. Merge or close it on GitHub.
|
||||
3. Wait up to ~5–6 minutes (sweep runs every 5 minutes, with a 30s startup delay). Observe the sidebar.
|
||||
|
||||
**Expected:** Local session whose PR is `merged` or `closed` is archived from the sidebar on the next sweep tick (≤ ~5 min) after the merge/close event. Cached PR-state lookups have a 1-hour cooldown for sessions whose state isn't yet terminal. Remote and SSH sessions are not affected.
|
||||
|
||||
**Diagnostics on failure:** Screenshot of sidebar, `gh pr view <num>` output (confirming merge state), launcher log, settings file content (`ccAutoArchiveOnPrClose`).
|
||||
|
||||
**References:** [Work in parallel with sessions](https://code.claude.com/docs/en/desktop#work-in-parallel-with-sessions)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:55269` — default `ccAutoArchiveOnPrClose: !1` setting.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:533517` — sweep cadence constants: `$3n = 300_000` ms (5 min interval), `W3n = 3_600_000` ms (1 h recheck cooldown), `Fst = 10` (concurrent batch size).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:533520` — `AutoArchiveEngine.start()` schedules the 5-min interval + 30s initial delay.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:533537` — `sweep()` gates on `Qi("ccAutoArchiveOnPrClose")` and archives sessions whose `prState` lowercases to `merged` or `closed` (`D3A` predicate at line 533607).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:533571` — `archiveSession(..., { cleanupWorktree: true })` removes the worktree alongside the archive.
|
||||
|
||||
## T31 — Side chat opens
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Side chat overlay
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, press `Ctrl+;` (or type `/btw` in the prompt).
|
||||
2. Ask a question in the side chat. Confirm the side chat sees the main thread context.
|
||||
3. Close the side chat. Confirm focus returns to the main session and the side chat content is not in the main thread.
|
||||
|
||||
**Expected:** Side chat opens, has access to main-thread context, but its replies do not appear in the main conversation. Closing returns focus.
|
||||
|
||||
**Diagnostics on failure:** Screenshot, launcher log, DevTools console.
|
||||
|
||||
**References:** [Ask a side question](https://code.claude.com/docs/en/desktop#ask-a-side-question-without-derailing-the-session)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:487025` — side-chat system-prompt suffix: "You are running in a side chat — a lightweight fork… nothing you say here lands in the main transcript."
|
||||
- `build-reference/app-extracted/.vite/build/index.js:487265` — `this.sideChats = new Map()` per-session fork registry.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:491658` — `startSideChat()` implementation; emits `side_chat_ready` / `side_chat_assistant` / `side_chat_turn_end` / `side_chat_closed` / `side_chat_error` events.
|
||||
- `build-reference/app-extracted/.vite/build/mainView.js:7506` — preload IPC bridges: `startSideChat`, `sendSideChatMessage`, `stopSideChat` (the renderer SPA wires `Ctrl+;` / `/btw` to these — UI lives in claude.ai's remote bundle, not build-reference).
|
||||
|
||||
## T32 — Slash command menu
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Code tab → Prompt slash menu
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, type `/` in the prompt box.
|
||||
2. Verify built-in commands, custom skills under `~/.claude/skills/`, project skills, and skills from installed plugins all appear.
|
||||
3. Select an entry — confirm it inserts as a highlighted token.
|
||||
|
||||
**Expected:** Slash menu lists every available command/skill. Selection inserts the token correctly.
|
||||
|
||||
**Diagnostics on failure:** Screenshot of slash menu, `ls ~/.claude/skills/`, project `.claude/skills/`, installed plugin manifest, launcher log.
|
||||
|
||||
**References:** [Use skills](https://code.claude.com/docs/en/desktop#use-skills)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:459463` — `getSupportedCommands({sessionId})` aggregates per-session `slashCommands` + cowork command registry (`p2()`) + built-ins (`Q_t`).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:332711` — `slashCommands: Di.array(Di.string()).optional()` schema field on the session record.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:377670` — `SkillManager` constructor: `skillDir = <agentDir>/.claude/skills`, `_discoverSkills()` walks project skills.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:444678` — private/public skill split under `<skillsRoot>/skills/{private,public}` for plugin-supplied skills.
|
||||
@@ -0,0 +1,168 @@
|
||||
# Distribution — DEB, RPM, AppImage
|
||||
|
||||
Tests covering Ubuntu/DEB-specific install behavior, Fedora/RPM-specific install behavior, AppImage fallback paths, and the auto-update interaction with system package managers. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## S01 — AppImage launches without manual `libfuse2t64` install
|
||||
|
||||
**Severity:** Critical (for Ubuntu users)
|
||||
**Surface:** AppImage runtime / FUSE
|
||||
**Applies to:** Ubu (and any Ubuntu 24.04+ host)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Fresh Ubuntu 24.04 install with default packages only.
|
||||
2. Download the project AppImage.
|
||||
3. Make executable and run it.
|
||||
|
||||
**Expected:** AppImage runs without first installing `libfuse2t64`. Either the AppImage bundles its own FUSE shim, the `.desktop`/postinst declares the dep, or the launcher gives a clear error pointing at the package name.
|
||||
|
||||
**Currently:** Fails on Ubuntu 24.04 with `dlopen(): error loading libfuse.so.2`. Workaround: `sudo apt install libfuse2t64`. Not yet filed.
|
||||
|
||||
**Diagnostics on failure:** Full stderr from the AppImage launch, `ldd ./claude-desktop-*.AppImage`, `dpkg -l | grep -i fuse`.
|
||||
|
||||
**References:** —
|
||||
|
||||
**Code anchors:** `scripts/packaging/appimage.sh:226` (downloads the upstream `appimagetool` AppImage as-is — no FUSE shim or static-mksquashfs bundling), `scripts/launcher-common.sh:64` (AppImage forces `--no-sandbox` "due to FUSE constraints"), `.github/workflows/test-artifacts.yml:47` (CI installs `libfuse2` before running the AppImage — i.e. the runtime hard-depends on libfuse2/libfuse2t64). No postinst dep declaration or user-facing FUSE error message exists.
|
||||
|
||||
## S02 — `XDG_CURRENT_DESKTOP=ubuntu:GNOME` doesn't break DE detection
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** DE detection / patch gate
|
||||
**Applies to:** Ubu
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. On Ubuntu 24.04 (where `XDG_CURRENT_DESKTOP=ubuntu:GNOME`), launch the app.
|
||||
2. Inspect launcher log for any DE-detection branches that should fire as GNOME.
|
||||
3. Audit `scripts/launcher-common.sh` and any DE-gated patches for string-equality checks against `XDG_CURRENT_DESKTOP`.
|
||||
|
||||
**Expected:** DE-detection logic handles Ubuntu's colon-separated value. `contains "GNOME"` or splitting on `:` is the safe pattern; `== "GNOME"` would miss Ubuntu.
|
||||
|
||||
**Diagnostics on failure:** `echo $XDG_CURRENT_DESKTOP`, the relevant launcher.sh code path, launcher log, the patches that ran or didn't.
|
||||
|
||||
**References:** Surfaced via session-capture review.
|
||||
|
||||
**Code anchors:** `scripts/launcher-common.sh:35-44` (Niri auto-detect lowercases `XDG_CURRENT_DESKTOP` and uses `*niri*` glob — handles colon-separated values), `scripts/patches/quick-window.sh:34-35` and `:117-118` (KDE gate uses `.toLowerCase().includes("kde")` — substring, not equality), `scripts/doctor.sh:304` (purely informational `_info "Desktop: $desktop"`, no branching). No `==` equality checks against `XDG_CURRENT_DESKTOP` exist anywhere in shell or patched JS.
|
||||
|
||||
## S03 — DEB install via APT pulls all required runtime deps
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** APT repository / dependency declarations
|
||||
**Applies to:** Ubu (any DEB-based distro)
|
||||
**Issues:** [`docs/learnings/apt-worker-architecture.md`](../../learnings/apt-worker-architecture.md)
|
||||
|
||||
**Steps:**
|
||||
1. Add the project's APT repo per the README install instructions.
|
||||
2. `sudo apt install claude-desktop-unofficial` on a fresh container/VM.
|
||||
3. Run `claude-desktop-unofficial` — first launch should succeed with no further package installs.
|
||||
|
||||
**Expected:** All transitive runtime deps are declared in the package and pulled by APT. First launch succeeds without manual `apt install` of any extra package.
|
||||
|
||||
**Diagnostics on failure:** `apt-cache depends claude-desktop-unofficial`, missing-library errors from the launcher, `ldd` against the binary.
|
||||
|
||||
**References:** [`docs/learnings/apt-worker-architecture.md`](../../learnings/apt-worker-architecture.md)
|
||||
|
||||
**Code anchors:** `scripts/packaging/deb.sh:185-197` (DEBIAN/control file — no `Depends:` field is emitted; relies on bundled Electron + the comment "No external dependencies are required at runtime" at line 183), `scripts/packaging/deb.sh:202-230` (postinst only sets chrome-sandbox suid, no dep-pull). Worker chain serving the package: `worker/src/worker.js:22-31` (`DEB_RE`) and `:33-43` (302 → GitHub Releases).
|
||||
|
||||
## S04 — RPM install via DNF pulls all required runtime deps
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** DNF repository / dependency declarations
|
||||
**Applies to:** KDE-W, KDE-X, GNOME, Sway, i3, Niri (any RPM-based distro)
|
||||
**Issues:** [`docs/learnings/apt-worker-architecture.md`](../../learnings/apt-worker-architecture.md) *(covers both APT and DNF)*
|
||||
|
||||
**Steps:**
|
||||
1. Add the project's DNF repo per the README.
|
||||
2. `sudo dnf install claude-desktop-unofficial` on a fresh container/VM.
|
||||
3. Run `claude-desktop-unofficial` — first launch should succeed.
|
||||
|
||||
**Expected:** All transitive runtime deps are declared in the RPM and pulled by DNF. First launch succeeds with no further package installs.
|
||||
|
||||
**Diagnostics on failure:** `dnf repoquery --requires claude-desktop-unofficial`, `rpm -qR claude-desktop-unofficial`, launcher missing-library errors.
|
||||
|
||||
**References:** [`docs/learnings/apt-worker-architecture.md`](../../learnings/apt-worker-architecture.md)
|
||||
|
||||
**Code anchors:** `scripts/packaging/rpm.sh:188` (`AutoReqProv: no` — explicitly disables RPM's auto-dep generation; spec declares no `Requires:`), `scripts/packaging/rpm.sh:194-198` (strip + build-id disabled because Electron binaries don't tolerate them — bundled approach). Worker chain: `worker/src/worker.js:28-31` (`RPM_RE`).
|
||||
|
||||
## S05 — Doctor recognises dnf-installed package, doesn't false-flag as AppImage
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Doctor package-format detection
|
||||
**Applies to:** KDE-W, KDE-X, GNOME, Sway, i3, Niri
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. On a Fedora/Nobara/RPM-based distro with claude-desktop-unofficial installed via dnf, run `claude-desktop-unofficial --doctor`.
|
||||
2. Look for the install-method line.
|
||||
|
||||
**Expected:** Doctor detects rpm install (e.g. via `rpm -qf` against the binary path) and reports it cleanly. No `not found via dpkg (AppImage?)` warning.
|
||||
|
||||
**Currently:** Addressed by #711 — the probe follows ownership: `rpm -qf` against the Electron binary first, `dpkg-query -W claude-desktop-unofficial` only when rpm does not own the path, so dnf installs report cleanly and a stale dpkg record cannot shadow a live rpm install. The `claude-desktop-unofficial not found via dpkg/rpm (AppImage?)` WARN fires only when neither manager owns the install. Needs a re-verify sweep on the affected rows ([T13](./launch.md#t13--doctor-reports-correct-package-format)).
|
||||
|
||||
**Diagnostics on failure:** Full `--doctor` output, `rpm -qf $(which claude-desktop-unofficial)`, the doctor source line that decides the format.
|
||||
|
||||
**References:** [T13](./launch.md#t13--doctor-reports-correct-package-format)
|
||||
|
||||
**Code anchors:** `scripts/doctor.sh:717-768` (`_doctor_check_pkg_version`) — rpm-first ownership probe with a dpkg fallback scoped to `claude-desktop-unofficial`; the not-found WARN is the last resort when neither manager owns the install.
|
||||
|
||||
## S15 — AppImage extraction (`--appimage-extract`) works as documented fallback
|
||||
|
||||
**Severity:** Could
|
||||
**Surface:** AppImage runtime / FUSE-less fallback
|
||||
**Applies to:** Any AppImage row
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. On a host without FUSE, run `./claude-desktop-*.AppImage --appimage-extract`.
|
||||
2. Inspect `squashfs-root/`.
|
||||
3. Run `squashfs-root/AppRun`.
|
||||
|
||||
**Expected:** Extraction completes. `squashfs-root/AppRun` launches the app cleanly without FUSE.
|
||||
|
||||
**Diagnostics on failure:** Extraction stderr, `ls squashfs-root/`, AppRun stderr.
|
||||
|
||||
**References:** Linked from the runtime error message when FUSE is missing.
|
||||
|
||||
**Code anchors:** `scripts/packaging/appimage.sh:282` and `:312` (built with stock `appimagetool`, which always supports `--appimage-extract`), `scripts/packaging/appimage.sh:70-118` (`AppRun` script that lives at `squashfs-root/AppRun` after extraction). CI exercises this path: `tests/test-artifact-appimage.sh:36-44` and `.github/workflows/ci.yml:388` both run `--appimage-extract` and assert `squashfs-root/` exists.
|
||||
|
||||
## S16 — AppImage mount cleans up on app exit
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** AppImage mount lifecycle
|
||||
**Applies to:** Any AppImage row
|
||||
**Issues:** [CLAUDE.md "Common Gotchas"](https://github.com/aaddrick/claude-desktop-debian/blob/main/CLAUDE.md)
|
||||
|
||||
**Steps:**
|
||||
1. Launch the AppImage. Confirm `mount | grep claude` shows the mount.
|
||||
2. Quit the app cleanly via tray → Quit (or `Ctrl+Q`).
|
||||
3. Re-run `mount | grep claude` — mount should be gone.
|
||||
|
||||
**Expected:** AppImage's mount at `/tmp/.mount_claude*` is unmounted and the directory removed when all child Electron processes exit. Stale mounts after force-quit are handled by `pkill -9 -f "mount_claude"` per CLAUDE.md but should not be the common case.
|
||||
|
||||
**Diagnostics on failure:** `mount | grep claude` after exit, `ls -la /tmp/.mount_claude*`, `pgrep -af claude`, `journalctl -k -n 50` for mount errors.
|
||||
|
||||
**References:** [CLAUDE.md "Common Gotchas"](https://github.com/aaddrick/claude-desktop-debian/blob/main/CLAUDE.md)
|
||||
|
||||
**Code anchors:** Mount lifecycle is owned by upstream `appimagetool`'s runtime, not this repo — `scripts/packaging/appimage.sh:282`/`:312` invokes the stock tool with no custom AppRun-side cleanup. `CLAUDE.md:179-183` documents `pkill -9 -f "mount_claude"` as the manual recovery for stale mounts after force-quit. No project-side unmount handler exists; the test asserts upstream behavior, not ours.
|
||||
|
||||
## S26 — Auto-update is disabled when installed via `apt` / `dnf`
|
||||
|
||||
> **⚠ Missing in build 1.5354.0** — No project-side suppression of upstream auto-update exists; the launcher exports `ELECTRON_FORCE_IS_PACKAGED=true`, which causes upstream's `lii()` gate to return true on Linux and the auto-update tick loop to start. Suppression is "accidental" — it relies on Electron's built-in `autoUpdater` module being unimplemented on Linux (so `setFeedURL`/`checkForUpdates` throw, the `error` listener logs, and no download happens). Tracked at [#567](https://github.com/aaddrick/claude-desktop-debian/issues/567); re-verify after next upstream bump.
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Auto-update path
|
||||
**Applies to:** All DEB/RPM rows
|
||||
**Issues:** [#567](https://github.com/aaddrick/claude-desktop-debian/issues/567)
|
||||
|
||||
**Steps:**
|
||||
1. Install via APT or DNF.
|
||||
2. Launch the app and let it sit for ~5 minutes.
|
||||
3. Inspect launcher log + filesystem for any auto-update download attempt.
|
||||
|
||||
**Expected:** When installed via the project's APT or DNF repo, the in-app auto-update path is suppressed. The app does not download replacement binaries (which would race the package manager). Updates flow through `apt upgrade` / `dnf upgrade` only. AppImage installs may continue to self-update or punt to the user.
|
||||
|
||||
**Diagnostics on failure:** Launcher log, network captures (look for downloads from `releases.anthropic.com` or `api.anthropic.com/api/desktop/linux/...`), filesystem changes under `~/.config/Claude/`.
|
||||
|
||||
**References:** [`docs/learnings/apt-worker-architecture.md`](../../learnings/apt-worker-architecture.md)
|
||||
|
||||
**Code anchors:** `scripts/launcher-common.sh:249` (`export ELECTRON_FORCE_IS_PACKAGED=true` — makes upstream think it's installed); `build-reference/app-extracted/.vite/build/index.js:508761-508769` (upstream `lii()` returns `hA.app.isPackaged` on Linux — passes the gate); `:508554-508559` (only suppression hook is enterprise-policy `disableAutoUpdates`, no Linux/distro carve-out); `:508770-508774` (feed URL `https://api.anthropic.com/api/desktop/linux/<arch>/squirrel/update?...`); `:508800-508803` (calls `hA.autoUpdater.setFeedURL` + `.checkForUpdates()` unconditionally on Linux). No patch in `scripts/patches/*.sh` neutralizes the autoUpdater module or sets `disableAutoUpdates`. AppImage continues to ship update info: `scripts/packaging/appimage.sh:308-309` (`gh-releases-zsync` zsync metadata embedded for releases).
|
||||
@@ -0,0 +1,153 @@
|
||||
# Extensibility — Plugins, MCP, Hooks, Memory
|
||||
|
||||
Tests covering the Anthropic & Partners plugin install flow, the plugin browser, MCP server config, hooks, `CLAUDE.md` memory loading, and per-user storage of plugins/worktrees. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T11 — Plugin install (Anthropic & Partners)
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Plugin browser → install flow
|
||||
**Applies to:** All rows
|
||||
**Issues:** [`docs/learnings/plugin-install.md`](../../learnings/plugin-install.md)
|
||||
|
||||
**Steps:**
|
||||
1. In a Code-tab session, click **+** → **Plugins** → **Add plugin**.
|
||||
2. Find an Anthropic & Partners plugin. Click **Install**.
|
||||
3. Verify it lands in **Manage plugins** and its skills appear in the slash menu.
|
||||
4. Re-install the same plugin to verify idempotence.
|
||||
|
||||
**Expected:** Install completes end-to-end: gate logic accepts, backend endpoint responds, plugin appears in the plugin list. Re-install is idempotent.
|
||||
|
||||
**Diagnostics on failure:** DevTools network panel during install, launcher log, `~/.claude/plugins/` content, the gate-logic code path (see learnings doc).
|
||||
|
||||
**References:** [`docs/learnings/plugin-install.md`](../../learnings/plugin-install.md), [Install plugins](https://code.claude.com/docs/en/desktop#install-plugins)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:507181` (`installPlugin` IPC + gate, with `pluginSource === "remote"` branch and CLI fallback); `:507193` log `[CustomPlugins] installPlugin: attempting remote API install`; `:465816` `dx()` returns `~/.claude/plugins`; `:465822` `installed_plugins.json` (idempotency record).
|
||||
|
||||
**Inventory anchor:** `…customize.main.navigation.button-by-name.add-plugin` (role `button`, label `Add plugin`); sibling `…button-by-name.browse-plugins` (label `Browse plugins`). Both are persistent in the Customize panel — anchors the entry-point click chain.
|
||||
|
||||
## T33 — Plugin browser
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Plugin browser UI
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Click **+** → **Plugins** → **Add plugin**.
|
||||
2. Confirm entries from the official Anthropic marketplace appear.
|
||||
3. Install a non-Anthropic plugin end-to-end.
|
||||
4. Verify it shows in **Manage plugins** and contributes its skills to the slash menu.
|
||||
|
||||
**Expected:** Plugin browser opens, shows the marketplace, install completes. Installed plugins appear under Manage plugins and contribute to the slash menu.
|
||||
|
||||
**Diagnostics on failure:** Screenshot of plugin browser, network captures, launcher log, `~/.claude/plugins/` listing.
|
||||
|
||||
**References:** [Install plugins](https://code.claude.com/docs/en/desktop#install-plugins)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:71392` (`CustomPlugins.listMarketplaces` IPC); `:71534` (`listAvailablePlugins` IPC); `:507176` (`listMarketplaces` main-process handler); `:496236` deep-link route `plugins/new` opens the browser surface.
|
||||
|
||||
**Inventory anchor:** `…customize.main.navigation.button-by-name.browse-plugins` (role `button`, label `Browse plugins`); sibling `…link-by-name.connectors` (role `link`, label `Connectors`). The browser surface itself (marketplace listings, install button) appears under a child dialog not captured at idle — re-capture with the dialog open to anchor those.
|
||||
|
||||
## T35 — MCP server config picked up
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** MCP / Code tab
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Add an MCP server to `~/.claude.json` or `<project>/.mcp.json`.
|
||||
2. Open a Code-tab session against the project.
|
||||
3. Type `/` in the prompt — verify MCP-provided tools appear in the slash menu (or invoke one directly).
|
||||
4. Separately, confirm `claude_desktop_config.json` (Chat-tab MCP) is **not** picked up by Code tab.
|
||||
|
||||
**Expected:** MCP servers in `~/.claude.json` or `.mcp.json` start when a Code session opens. Tools appear in the slash menu, calls succeed end-to-end. `claude_desktop_config.json` is separate per upstream docs.
|
||||
|
||||
**Diagnostics on failure:** Server stderr (MCP servers log to stderr), `~/.claude.json` and `.mcp.json` content, launcher log, DevTools console for MCP wire errors.
|
||||
|
||||
**References:** [MCP servers: desktop chat app vs Claude Code](https://code.claude.com/docs/en/desktop#shared-configuration), [`docs/learnings/plugin-install.md`](../../learnings/plugin-install.md)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:215418` (Code-tab loads `<project>/.mcp.json` per scanned dir); `:176766` reads `~/.claude.json`; `:489098` Code-session passes `settingSources: ["user", "project", "local"]` to the agent SDK; `:130821` `claude_desktop_config.json` is the chat-tab path constant (separate userData dir at `:130829` `kee()`), confirming the two trees do not overlap.
|
||||
|
||||
## T36 — Hooks fire
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Hooks runtime
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Add a `SessionStart` hook in `~/.claude/settings.json` that writes a marker file.
|
||||
2. Open a new Code-tab session.
|
||||
3. Confirm the marker file exists.
|
||||
4. Repeat with `PreToolUse` / `PostToolUse` hooks. Switch transcript view to Verbose to see the hook output.
|
||||
|
||||
**Expected:** Hooks defined in `~/.claude/settings.json` execute at the documented points. Hook output is visible in Verbose transcript mode. A failing hook surfaces a clear error rather than silently breaking the session.
|
||||
|
||||
**Diagnostics on failure:** Hook script stderr, marker file presence, launcher log, settings file content, Verbose transcript output.
|
||||
|
||||
**References:** [Shared configuration](https://code.claude.com/docs/en/desktop#shared-configuration)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:489098` Code-session sets `settingSources: ["user", "project", "local"]` (agent SDK reads `~/.claude/settings.json` hooks from this); `:455717` built-in `PreToolUse` hooks registry the runtime extends; `:455819` `UserPromptSubmit`; `:465680` `PostToolUse`; `:465754` `Stop`; `:493411` runtime emits `hook_started` / `hook_progress` / `hook_response` for `SessionStart` (Verbose transcript path).
|
||||
|
||||
## T37 — `CLAUDE.md` memory loads
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Memory / Code tab session prompt
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Confirm a project `CLAUDE.md` exists at the working folder.
|
||||
2. Confirm `~/.claude/CLAUDE.md` exists with at least one identifying token.
|
||||
3. Open a Code-tab session against the project.
|
||||
4. Ask Claude "what's in your CLAUDE.md" — verify the response matches on-disk content.
|
||||
5. Edit `CLAUDE.md`. Start a new session — verify the new content is loaded.
|
||||
|
||||
**Expected:** Project `CLAUDE.md` and `CLAUDE.local.md` at the working folder, plus `~/.claude/CLAUDE.md`, are loaded into the session's system prompt. Updates after edit on the next session start.
|
||||
|
||||
**Diagnostics on failure:** `cat CLAUDE.md` and `cat ~/.claude/CLAUDE.md` outputs, launcher log, system-prompt dump if accessible (Verbose transcript may show it).
|
||||
|
||||
**References:** [Shared configuration](https://code.claude.com/docs/en/desktop#shared-configuration)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:259691` working-dir scan reads `CLAUDE.md` and `.claude/CLAUDE.md`; `:455188` global account memory `zhA(accountId, orgId)` is copied to the per-session `.claude/CLAUDE.md` at session start (`[GlobalMemory] Copied CLAUDE.md`); `:283107` `cE()` resolves `CLAUDE_CONFIG_DIR` or `~/.claude`, the dir whose `CLAUDE.md` the agent SDK loads via `settingSources: ["user", ...]` (see T36 anchor at `:489098`).
|
||||
|
||||
## S27 — Plugins install per-user, not into system paths
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Plugin storage
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. As a non-root user, install a plugin via the desktop plugin browser.
|
||||
2. Inspect `~/.claude/plugins/` for the install.
|
||||
3. Verify nothing was written under `/usr` or other system-managed trees (`find /usr -newer /tmp/marker -name '*claude*' 2>/dev/null` after `touch /tmp/marker; install plugin`).
|
||||
|
||||
**Expected:** Plugins land under `~/.claude/plugins/` (or the equivalent per-user dir). Never under `/usr`. Non-root install/enable/disable works without `sudo`.
|
||||
|
||||
**Diagnostics on failure:** `find / -name '*<plugin-name>*' 2>/dev/null`, install logs, launcher log.
|
||||
|
||||
**References:** [Install plugins](https://code.claude.com/docs/en/desktop#install-plugins)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:283107` `cE()` resolves the config root to `CLAUDE_CONFIG_DIR` or `~/.claude` — never `/usr`; `:465815` `dx()` returns `<cE()>/plugins`; `:465821`/`:465824`/`:465827` `installed_plugins.json`, `known_marketplaces.json`, `marketplaces/` all sit under `dx()`. No system-path writes in the install path.
|
||||
|
||||
## S28 — Worktree creation surfaces clear error on read-only mounts
|
||||
|
||||
**Severity:** Could
|
||||
**Surface:** Worktree creation on read-only filesystem
|
||||
**Applies to:** All rows (NixOS users hit this most often)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Place a project on a read-only mount (e.g. squashfs, NFS read-only export, `mount -o ro` bind).
|
||||
2. Open a Code-tab session against it.
|
||||
3. Try to start a parallel session that needs a worktree.
|
||||
|
||||
**Expected:** Worktree creation fails with a clear error pointing at the read-only mount. No silent loss of work, no writes to a wrong directory, no parent-repo corruption.
|
||||
|
||||
**Diagnostics on failure:** `mount | grep <project-path>`, `git worktree add` direct invocation (does it fail the same way?), launcher log, screenshot of error dialog.
|
||||
|
||||
**References:** [Work in parallel with sessions](https://code.claude.com/docs/en/desktop#work-in-parallel-with-sessions)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:462841` worktree parent dir is `<repo>/.claude/worktrees` (or `chillingSlothLocation.customPath` override at `:462836`); `:462928` `git worktree add` failure path returns `null` after `R.error("Failed to create git worktree: …")`; `:462760` `Sbn()` classifies "Permission denied" / "Access is denied" / "could not lock config file" as `"permission-denied"` (the read-only-mount taxonomy bucket).
|
||||
@@ -0,0 +1,77 @@
|
||||
# Launch & Process Lifecycle
|
||||
|
||||
Tests covering app startup, the `--doctor` health check, package-format detection, and multi-instance behavior. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T01 — App launch
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** App startup
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
**Runner:** [`tools/test-harness/src/runners/T01_app_launch.spec.ts`](../../../tools/test-harness/src/runners/T01_app_launch.spec.ts)
|
||||
|
||||
**Steps:**
|
||||
1. From a clean session, run `claude-desktop-unofficial` (deb/rpm) or launch the AppImage.
|
||||
2. Wait up to 10 seconds.
|
||||
|
||||
**Expected:** Main window opens within ~10s. No error toast, no crash. The launcher log at `~/.cache/claude-desktop-debian/launcher.log` shows the expected backend selection (`Using X11 backend via XWayland` on Wayland sessions, or native Wayland when forced).
|
||||
|
||||
**Diagnostics on failure:** Launcher log, `--doctor` output, session env (`XDG_SESSION_TYPE`, `XDG_CURRENT_DESKTOP`), `dmesg | tail -50`, any crash report under `~/.config/Claude/logs/`.
|
||||
|
||||
**References:** —
|
||||
**Code anchors:** `scripts/launcher-common.sh:98` (X11-via-XWayland log line), `scripts/launcher-common.sh:102` (native-Wayland log line), `build-reference/app-extracted/.vite/build/index.js:524875` (`app.on("ready")` registration), `build-reference/app-extracted/.vite/build/index.js:524881-524931` (main `BrowserWindow` factory `Ori()` — `titleBarStyle`, mainWindow.js preload, initial `show`).
|
||||
|
||||
## T02 — Doctor health check
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** CLI / `--doctor`
|
||||
**Applies to:** All rows
|
||||
**Issues:** [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538)
|
||||
|
||||
**Steps:**
|
||||
1. Run `claude-desktop-unofficial --doctor`.
|
||||
2. Inspect exit code (`echo $?`) and stdout/stderr.
|
||||
|
||||
**Expected:** Exits 0. All checks PASS or report expected WARN. No FAIL checks. Doctor currently reports display-server, menu-bar mode, Electron path/version, Chrome sandbox perms, SingletonLock, MCP config, Node.js, desktop entry, disk space, and a Cowork section — it does **not** surface the resolved titlebar style. See also [T13](#t13--doctor-reports-correct-package-format) for the package-format detection slice.
|
||||
|
||||
**Diagnostics on failure:** Full `--doctor` output, the install path being inspected (`which claude-desktop-unofficial`), package metadata (`dpkg -S` / `rpm -qf` against the binary).
|
||||
|
||||
**References:** [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538)
|
||||
**Code anchors:** `scripts/doctor.sh:280` (`run_doctor` entry point), `scripts/doctor.sh:301-319` (display-server check), `scripts/doctor.sh:401-417` (SingletonLock check), `scripts/doctor.sh:744-753` (exit-code summary).
|
||||
|
||||
## T13 — Doctor reports correct package format
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** CLI / `--doctor`
|
||||
**Applies to:** All rows (currently `✗` on every Fedora row — see [S05](./distribution.md#s05--doctor-recognises-dnf-installed-package-doesnt-false-flag-as-appimage))
|
||||
**Issues:** — *(no issue filed; surfaced via session-capture review)*
|
||||
|
||||
**Steps:**
|
||||
1. Install via the relevant package manager (`apt` / `dnf`) or AppImage.
|
||||
2. Run `claude-desktop-unofficial --doctor` and look for the install-method line.
|
||||
|
||||
**Expected:** Doctor identifies the install method correctly. On RPM-based distros (Fedora, Nobara) it does **not** report `not found via dpkg/rpm (AppImage?)` for a dnf install. On DEB-based distros it does not assume AppImage when dpkg returns the package metadata.
|
||||
|
||||
**Diagnostics on failure:** `dpkg -S $(which claude-desktop-unofficial)`, `rpm -qf $(which claude-desktop-unofficial)`, full `--doctor` output, the line of doctor source that decides the format.
|
||||
|
||||
**References:** [S05](./distribution.md#s05--doctor-recognises-dnf-installed-package-doesnt-false-flag-as-appimage)
|
||||
**Code anchors:** `scripts/doctor.sh:717-768` (`_doctor_check_pkg_version`) — since #711 the probe follows ownership: `rpm -qf` against the Electron binary first (only the database that installed the file can claim it), then `dpkg-query -W claude-desktop-unofficial` when rpm does not own the path. The `claude-desktop-unofficial not found via dpkg/rpm (AppImage?)` WARN fires only when neither manager owns the install.
|
||||
|
||||
## T14 — Multi-instance behavior
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** App lifecycle
|
||||
**Applies to:** All rows
|
||||
**Issues:** [PR #536](https://github.com/aaddrick/claude-desktop-debian/pull/536) (closed, docs-only — no in-tree opt-in flag)
|
||||
|
||||
**Steps:**
|
||||
1. Launch `claude-desktop-unofficial`. Wait for the main window.
|
||||
2. Launch `claude-desktop-unofficial` again from another terminal or `.desktop` invocation.
|
||||
3. Optionally: follow the manual `--user-data-dir` recipe sketched in PR #536 (separate Electron `userData` per profile so each gets its own `SingletonLock` — note the PR was closed, the recipe is not shipped in-tree).
|
||||
|
||||
**Expected:** Second invocation focuses the existing window — no new process. The launcher's `cleanup_stale_lock` removes a `SingletonLock` whose owning PID is no longer running. With separate `--user-data-dir` per profile (manual workaround, not an in-tree feature), each profile runs an independent Electron instance.
|
||||
|
||||
**Diagnostics on failure:** `pgrep -af claude-desktop`, `ls -la ~/.config/Claude/SingletonLock`, launcher log, any "another instance is running" dialog text.
|
||||
|
||||
**References:** [PR #536](https://github.com/aaddrick/claude-desktop-debian/pull/536)
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:525162-525173` (`requestSingleInstanceLock()` + `app.on("second-instance", ...)` — shows existing window, restores if minimized, focuses), `build-reference/app-extracted/.vite/build/index.js:525204-525207` (early-return on lost lock at `app.on("ready")`), `scripts/launcher-common.sh:187-208` (`cleanup_stale_lock` — drops a `SingletonLock` symlink whose `hostname-PID` target points at a dead PID).
|
||||
@@ -0,0 +1,282 @@
|
||||
# Platform Integration
|
||||
|
||||
Tests covering autostart, Cowork integration, WebGL graceful degradation, `.desktop`-launch env inheritance, encrypted env-var storage, the macOS/Windows-only Computer Use feature, and Dispatch session pairing. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T09 — AutoStart via XDG
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** XDG Autostart
|
||||
**Applies to:** All rows
|
||||
**Issues:** [PR #450](https://github.com/aaddrick/claude-desktop-debian/pull/450)
|
||||
|
||||
**Steps:**
|
||||
1. In Settings, toggle "Open at Login" / "Start at boot" ON.
|
||||
2. Inspect `~/.config/autostart/` for a `.desktop` entry.
|
||||
3. Logout/login. Verify app launches automatically.
|
||||
4. Toggle OFF. Verify the autostart entry is removed.
|
||||
|
||||
**Expected:** Toggling ON creates a `~/.config/autostart/*.desktop` entry that is XDG-spec compliant (not a custom systemd unit or shell hook). After login, app launches automatically. Toggling OFF removes the entry.
|
||||
|
||||
**Diagnostics on failure:** `ls -la ~/.config/autostart/`, content of the .desktop file, `desktop-file-validate` on it, launcher log.
|
||||
|
||||
**References:** [PR #450](https://github.com/aaddrick/claude-desktop-debian/pull/450)
|
||||
|
||||
**Code anchors:**
|
||||
- `scripts/frame-fix-wrapper.js:376` — XDG Autostart shim
|
||||
intercepting `app.{get,set}LoginItemSettings` (writes/removes
|
||||
`$XDG_CONFIG_HOME/autostart/claude-desktop.desktop`).
|
||||
- `scripts/frame-fix-wrapper.js:429` — `buildAutostartContent()`
|
||||
emits the spec-compliant `[Desktop Entry]` block.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:524205` —
|
||||
upstream `isStartupOnLoginEnabled` / `setStartupOnLoginEnabled` IPC
|
||||
surface that the wrapper interposes on.
|
||||
|
||||
## T10 — Cowork integration
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Cowork tab + VM daemon
|
||||
**Applies to:** All rows
|
||||
**Issues:** [`docs/learnings/cowork-vm-daemon.md`](../../learnings/cowork-vm-daemon.md)
|
||||
|
||||
**Steps:**
|
||||
1. Sign into the app. Open the Cowork tab.
|
||||
2. Confirm Cowork-specific UI renders (ghost icon in topbar, Cowork menus).
|
||||
3. Trigger a Cowork action that needs the VM daemon.
|
||||
4. Kill the VM daemon process; verify it respawns within the documented timeout.
|
||||
|
||||
**Expected:** Cowork features render. VM daemon spawns when needed, files are visible, daemon respawns within the documented timeout if it crashes.
|
||||
|
||||
**Diagnostics on failure:** `pgrep -af cowork`, daemon logs, launcher log, the respawn-logic code path (see learnings doc).
|
||||
|
||||
**References:** [`docs/learnings/cowork-vm-daemon.md`](../../learnings/cowork-vm-daemon.md)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:143371` —
|
||||
upstream's Windows named-pipe path (`\\.\pipe\cowork-vm-service`)
|
||||
that `scripts/patches/cowork.sh` Patch 1 rewrites to
|
||||
`$XDG_RUNTIME_DIR/cowork-vm-service.sock`.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:143453` —
|
||||
`kUe()` retry loop (5 attempts, 1 s gap) that the auto-launch
|
||||
injection from Patch 6 piggybacks on after the rewrite.
|
||||
- `scripts/patches/cowork.sh:244` — Patch 6 (auto-launch + stdio
|
||||
pipe + 10 s rate-limited respawn — issue #408).
|
||||
- `scripts/patches/cowork.sh:365` — Patch 6b (extends the
|
||||
reinstall-delete list with `sessiondata.img` / `rootfs.img.zst`
|
||||
so a wedged daemon can self-recover).
|
||||
|
||||
## T12 — WebGL warn-only
|
||||
|
||||
**Severity:** Could
|
||||
**Surface:** Chromium GPU diagnostics
|
||||
**Applies to:** All rows (especially VM rows and hybrid-GPU laptops)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch the app. Open DevTools → navigate to `chrome://gpu`.
|
||||
2. Inspect WebGL1/WebGL2 status.
|
||||
3. Use the app for ~5 minutes — exercise UI, sidebar, settings.
|
||||
|
||||
**Expected:** WebGL1/2 may report as blocklisted (typical on virtio-gpu in VMs and on hybrid GPU laptops). This is informational. UI continues to render without graphical glitches; no feature is broken by the blocklist.
|
||||
|
||||
**Diagnostics on failure:** `chrome://gpu` full content, screenshot of any visual glitch, `glxinfo | head -20` (X11) or `eglinfo` (Wayland), `lspci -k | grep -A2 VGA`.
|
||||
|
||||
**References:** —
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:524809` —
|
||||
`app.disableHardwareAcceleration()` is gated on the user-toggleable
|
||||
`isHardwareAccelerationDisabled` setting; upstream does not pass
|
||||
`--ignore-gpu-blocklist` or `--use-gl=*`, so chrome://gpu reflects
|
||||
Chromium's stock blocklist behaviour.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:500571` —
|
||||
the only `webgl:!1` override is scoped to the feedback popup
|
||||
(`in-memory-feedback` partition); main UI does not disable WebGL.
|
||||
|
||||
## S17 — App launched from `.desktop` inherits shell `PATH`
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** `.desktop`-launch env handling
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Configure `~/.bashrc` (or `~/.zshrc`) with `export PATH="$HOME/.custom-bin:$PATH"` and a custom binary in that dir.
|
||||
2. Launch the app via dmenu/krunner/GNOME Activities/Plasma launcher (i.e. **not** from a terminal).
|
||||
3. Open a Code-tab terminal pane. Run `which <custom-binary>`.
|
||||
4. Repeat for `npm`, `node`, `git`, `gh`.
|
||||
|
||||
**Expected:** Code session can find tools defined in the user's shell profile, even when the app was launched non-interactively. Either the launcher script sources the user's shell profile, or the app reads `~/.bashrc` / `~/.zshrc` to extract `PATH` the way macOS does.
|
||||
|
||||
**Diagnostics on failure:** `echo $PATH` from inside the integrated terminal, the env passed to the app process (`cat /proc/$(pgrep -f electron)/environ | tr '\0' '\n' | grep PATH`), launcher log.
|
||||
|
||||
**References:** [Local sessions](https://code.claude.com/docs/en/desktop#local-sessions), [Session not finding installed tools](https://code.claude.com/docs/en/desktop#session-not-finding-installed-tools)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259300` —
|
||||
`SLr()` resolves the bundled `shell-path-worker/shellPathWorker.js`.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259349` —
|
||||
`NLr()` forks it via `utilityProcess.fork`; on success
|
||||
`FX()` (line 259311) merges the extracted env into `process.env`.
|
||||
- `build-reference/app-extracted/.vite/build/shell-path-worker/shellPathWorker.js:205`
|
||||
— `extractPathFromShell()` runs the user's login shell (`-l -i`)
|
||||
and parses the printed `$PATH` between sentinels (mac-style env
|
||||
inheritance now applied on Linux too).
|
||||
|
||||
## S18 — Local environment editor persists across reboot
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Local env editor / encrypted store
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Open the local environment editor. Add `TEST_VAR=hello`.
|
||||
2. Restart the app — verify variable is still there.
|
||||
3. Reboot the host. Sign back in. Verify variable is still there.
|
||||
|
||||
**Expected:** Variables saved via the local environment editor (per-app, encrypted) survive a logout/login cycle and a full reboot. On Linux this implies the encrypted store is wired to libsecret / kwallet / gnome-keyring and unlocks at session start.
|
||||
|
||||
**Diagnostics on failure:** `secret-tool search` (libsecret), `kwallet5-query` (KDE), `seahorse` UI inspection (GNOME), launcher log, the env-editor IPC call.
|
||||
|
||||
**References:** [Local sessions](https://code.claude.com/docs/en/desktop#local-sessions)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259251` —
|
||||
`I2t = new K_({ name: "ccd-environment-config", ... })` electron-store
|
||||
backing file (`~/.config/Claude/ccd-environment-config.json`).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259253` —
|
||||
`hLr()` writes via `safeStorage.encryptString` (libsecret on Linux).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:259268` —
|
||||
`J1()` decrypts on read; bails to `{}` if `safeStorage` reports
|
||||
encryption unavailable (no keyring backend running).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:70782` —
|
||||
`LocalSessionEnvironment.save` IPC entry that calls into `hLr`.
|
||||
|
||||
## S22 — Computer-use toggle is absent or visibly disabled on Linux
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Settings → Desktop app → General
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Open Settings → Desktop app → General.
|
||||
2. Look for the "Computer use" toggle.
|
||||
|
||||
**Expected:** Toggle either does not render on Linux, or renders as a disabled control with a clear "not supported on Linux" hint. Must not appear functional and silently fail (e.g. flip on but never produce screen-control behavior).
|
||||
|
||||
**Diagnostics on failure:** Screenshot of the Settings page, DevTools inspection of the toggle DOM (is it conditionally hidden? disabled? always-rendered?), launcher log.
|
||||
|
||||
**References:** [Let Claude use your computer](https://code.claude.com/docs/en/desktop#let-claude-use-your-computer), [Dispatch and computer use](https://claude.com/blog/dispatch-and-computer-use)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:240557` —
|
||||
`qDA = new Set(["darwin", "win32"])` excludes Linux from the
|
||||
computer-use platform set.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:241190` —
|
||||
`TF()` (the master enable check) short-circuits to `false` when
|
||||
`qDA.has(process.platform)` is false, so toggling
|
||||
`chicagoEnabled` on Linux can't activate the feature.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:242387` —
|
||||
`tvr()` returns `{ status: "unsupported", reason: "Computer use
|
||||
is not available on this platform", unsupportedCode:
|
||||
"unsupported_platform" }` for the Settings UI — confirms the
|
||||
toggle should render with a platform-unavailable hint, not silent
|
||||
failure.
|
||||
|
||||
## S23 — Dispatch-spawned sessions don't soft-lock on a never-approvable computer-use prompt
|
||||
|
||||
**Severity:** Critical (for Dispatch users)
|
||||
**Surface:** Dispatch session lifecycle on Linux
|
||||
**Applies to:** All rows with Dispatch enabled
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. From a paired phone, dispatch a task that would invoke computer use.
|
||||
2. Observe the Code-tab session that spawns on the desktop.
|
||||
3. Try to interact with other parts of the app.
|
||||
|
||||
**Expected:** Permission prompt times out or denies cleanly rather than hanging the session indefinitely. User can continue interacting with the rest of the app.
|
||||
|
||||
**Diagnostics on failure:** Screenshot of session state, launcher log, sidebar state (is the Dispatch session blocking the whole sidebar?), `pgrep -af claude`.
|
||||
|
||||
**References:** [Sessions from Dispatch](https://code.claude.com/docs/en/desktop#sessions-from-dispatch)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:512789` —
|
||||
`tool_permission_request` notification handler explicitly skips
|
||||
`toolName.startsWith("computer:")`, so the desktop never queues a
|
||||
user-facing prompt for computer-use tool calls (which couldn't run
|
||||
on Linux anyway — see S22).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:241190` —
|
||||
`TF()` gates computer-use execution off entirely on Linux, so a
|
||||
Dispatch-spawned session that requests it should hit the upstream
|
||||
"Set up computer use" remote-client setup card
|
||||
(`index.js:330114`) rather than block on a desktop prompt.
|
||||
|
||||
## S24 — Dispatch-spawned Code session appears with badge and notification
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Dispatch handoff
|
||||
**Applies to:** All rows with Dispatch enabled
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. From a paired phone, dispatch a task that routes to Code (e.g. "fix this bug").
|
||||
2. Observe the desktop sidebar.
|
||||
3. Confirm a desktop notification fires.
|
||||
4. Open the session and confirm 30-min approval expiry per upstream docs.
|
||||
|
||||
**Expected:** Dispatch task creates a sidebar entry tagged **Dispatch**, posts a desktop notification, and lands ready for review. App-permission approvals on this session expire after 30 minutes per upstream docs.
|
||||
|
||||
**Diagnostics on failure:** Screenshot of sidebar (badge present?), notification daemon state, launcher log, the Dispatch pairing config under `~/.config/Claude/`.
|
||||
|
||||
**References:** [Sessions from Dispatch](https://code.claude.com/docs/en/desktop#sessions-from-dispatch), [Dispatch and computer use](https://claude.com/blog/dispatch-and-computer-use)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:144561` —
|
||||
`Sd = "dispatch_child"` session-type constant.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:512200` —
|
||||
`onRemoteSessionStart` IPC routes a Dispatch-initiated child
|
||||
session into the local sidebar via `dispatchOnRemoteSessionStart`.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:285621` —
|
||||
`notifyDispatchParentIfNeeded()` posts the
|
||||
`Task "<title>" <state>` meta-notification when the dispatch
|
||||
child finishes (lands the result in the parent thread's
|
||||
notification queue).
|
||||
- `build-reference/app-extracted/.vite/build/index.js:285954` —
|
||||
`kind:"dispatch_child"` is the sidebar badge tag.
|
||||
|
||||
## S25 — Mobile pairing survives Linux session restart
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Dispatch pairing persistence
|
||||
**Applies to:** All rows with Dispatch enabled
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Pair the desktop with a phone.
|
||||
2. Quit the app fully. Re-launch.
|
||||
3. Try a Dispatch task. Verify pairing still works without re-pairing.
|
||||
4. Logout/login the desktop. Re-test.
|
||||
|
||||
**Expected:** Pairing remains active across app restart and logout/login. Pairing token is stored under `~/.config/Claude/` (or wherever the secure store lives) and survives.
|
||||
|
||||
**Diagnostics on failure:** `ls -la ~/.config/Claude/`, secret-store inspection, launcher log, pairing-flow IPC.
|
||||
|
||||
**References:** [Sessions from Dispatch](https://code.claude.com/docs/en/desktop#sessions-from-dispatch)
|
||||
|
||||
**Code anchors:**
|
||||
- `build-reference/app-extracted/.vite/build/index.js:511984` —
|
||||
`ZEe = "coworkTrustedDeviceToken"` electron-store key for the
|
||||
trusted-device token.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:511989` —
|
||||
`oYn()` writes the token via `safeStorage.encryptString` (libsecret
|
||||
on Linux); `aYn()` (`:512003`) decrypts on read.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:512022` —
|
||||
`gYn()` re-enrolls via `POST /api/auth/trusted_devices` only when
|
||||
there's no cached token, so a successful pair survives restart.
|
||||
- `build-reference/app-extracted/.vite/build/index.js:330229` —
|
||||
`_5r = "bridge-state.json"` (per-org/account bridge state under
|
||||
`~/.config/Claude/bridge-state.json`); `JF()`/`X0A()` at `:330230`
|
||||
read/locate it.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Routines & Scheduled Tasks
|
||||
|
||||
Tests covering the Routines page, scheduled task firing, catch-up runs after suspend, and the suspend-inhibit toggle. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T26 — Routines page renders
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Routines page
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Sign into the app, open the Code tab.
|
||||
2. Click **Routines** in the sidebar.
|
||||
3. Click **New routine** → **Local**.
|
||||
|
||||
**Expected:** Routines list opens. New-routine form shows all schedule presets (Manual, Hourly, Daily, Weekdays, Weekly), permission-mode picker, model picker, working-folder picker, and worktree toggle.
|
||||
|
||||
**Diagnostics on failure:** Screenshot of the Routines page (or the failure state), DevTools console output, launcher log, network captures of the routines API call (`mitmproxy` or DevTools network panel).
|
||||
|
||||
**References:** [Schedule recurring tasks](https://code.claude.com/docs/en/desktop-scheduled-tasks)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:507710` (create payload — `permissionMode`, `model`, `userSelectedFolders`, `useWorktree`, `cronExpression`, `fireAt`); `build-reference/app-extracted/.vite/build/index.js:280299` (`@hourly: "0 * * * *"` preset)
|
||||
|
||||
**Inventory anchors:** `root.complementary.button-by-name.routines` (sidebar entry); `root.complementary.button-by-name.routines.main.region.button-by-name.new-routine` (form trigger); siblings `…button-by-name.all`, `…button-by-name.calendar` (list-view tabs). Preset list (Hourly/Daily/etc.) lives inside the New-routine modal and is not in the idle-state inventory — re-capture with the modal open to anchor.
|
||||
|
||||
## T27 — Scheduled task fires and notifies
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Routines runtime + libnotify
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Create a Manual task with a simple instruction (e.g. "echo hello").
|
||||
2. Click **Run now**. Observe.
|
||||
3. Optionally: create an Hourly task and verify across the next hour boundary.
|
||||
|
||||
**Expected:** A fresh session starts, appears in the **Scheduled** section of the sidebar, and posts a desktop notification when it begins. Subsequent runs respect the deterministic offset described in upstream docs.
|
||||
|
||||
**Diagnostics on failure:** Launcher log, screenshot of sidebar, `gdbus call --session --dest=org.freedesktop.Notifications --object-path=/org/freedesktop/Notifications --method=org.freedesktop.DBus.Introspectable.Introspect` (verify daemon present), task SKILL.md content under `~/.claude/scheduled-tasks/<task-name>/`.
|
||||
|
||||
**References:** [How scheduled tasks run](https://code.claude.com/docs/en/desktop-scheduled-tasks#how-scheduled-tasks-run)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:282332` (`runNow(A)` — manual dispatch); `build-reference/app-extracted/.vite/build/index.js:512837` (`Rc.showNotification(...,scheduled-${l},...)` — desktop notification on completion); `build-reference/app-extracted/.vite/build/index.js:282654` (`getJitterSecondsForTask` — deterministic per-task offset via `v2r(A, n*60)`, capped by `dispatchJitterMaxMinutes` default 10)
|
||||
|
||||
## T28 — Scheduled task catch-up after suspend
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Routines runtime / wake-from-suspend
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Create an Hourly task.
|
||||
2. Suspend the host (`systemctl suspend`).
|
||||
3. Wait past at least one hourly slot. Wake the host.
|
||||
4. Observe whether a catch-up run starts.
|
||||
|
||||
**Expected:** Exactly one catch-up run for the most recently missed slot (older missed slots are discarded). Notification announces the catch-up. Missed runs older than seven days are not retried.
|
||||
|
||||
**Diagnostics on failure:** Task history in the routines detail page, launcher log, `journalctl --since="-1 day" | grep -i suspend`.
|
||||
|
||||
**References:** [Missed runs](https://code.claude.com/docs/en/desktop-scheduled-tasks#missed-runs)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:281695` (`R2r` — walks back from now, capped at `10080 * 60 * 1e3` ms = 7 days, returns at most one missed slot, dedupes by `IfA` bucket-key); `build-reference/app-extracted/.vite/build/index.js:281942` (`scheduledTaskPostWakeDelayMs` default 60000 ms — gates dispatch after `powerMonitor.on("resume")`); `build-reference/app-extracted/.vite/build/index.js:282569` (catch-up branch: `c ? 0 : this.getJitterSecondsForTask(o.id)` — missed-slot dispatch skips jitter)
|
||||
|
||||
## S19 — `CLAUDE_CONFIG_DIR` redirects scheduled-task storage
|
||||
|
||||
**Severity:** Could
|
||||
**Surface:** Config dir env var
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. In the local environment editor, set `CLAUDE_CONFIG_DIR=/some/other/path`.
|
||||
2. Restart the app.
|
||||
3. Create a scheduled task. Inspect filesystem.
|
||||
|
||||
**Expected:** Tasks resolve under `${CLAUDE_CONFIG_DIR}/scheduled-tasks/<task-name>/SKILL.md` rather than `~/.claude/scheduled-tasks/`. Pre-existing tasks under the old path are not silently dropped.
|
||||
|
||||
**Diagnostics on failure:** `ls -la ${CLAUDE_CONFIG_DIR}/scheduled-tasks/` and `~/.claude/scheduled-tasks/`, launcher log, env dump.
|
||||
|
||||
**References:** [Manage scheduled tasks](https://code.claude.com/docs/en/desktop-scheduled-tasks#manage-scheduled-tasks)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:283108` (`cE()` — resolves `process.env.CLAUDE_CONFIG_DIR ?? ~/.claude`, handles `~` prefix); `build-reference/app-extracted/.vite/build/index.js:283118` (`Tce()` — returns `${cE()}/scheduled-tasks`); `build-reference/app-extracted/.vite/build/index.js:488317` and `:509032` (call sites passing `taskFilesDir: Tce()` into the scheduled-tasks substrate)
|
||||
|
||||
## S20 — "Keep computer awake" inhibits idle suspend
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Suspend inhibitor
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Open Settings → Desktop app → General → "Keep computer awake". Toggle ON.
|
||||
2. Run `systemd-inhibit --list`. Look for a Claude-owned lock with `idle:sleep` what.
|
||||
3. Toggle OFF. Re-run `systemd-inhibit --list` — lock should be gone.
|
||||
|
||||
**Expected:** Toggling ON registers `systemd-inhibit --what=idle:sleep` (or the `org.freedesktop.PowerManagement.Inhibit` DBus call). Toggling OFF releases the lock.
|
||||
|
||||
**Diagnostics on failure:** `systemd-inhibit --list` before/after, `busctl --user tree org.freedesktop.PowerManagement` (if the path uses that backend), launcher log, the relevant settings IPC call.
|
||||
|
||||
**References:** [How scheduled tasks run](https://code.claude.com/docs/en/desktop-scheduled-tasks#how-scheduled-tasks-run)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:241897` (`hA.powerSaveBlocker.start("prevent-app-suspension")` — single block call, ref-counted by `PhA` Set); `build-reference/app-extracted/.vite/build/index.js:241905` (`hA.powerSaveBlocker.stop(BP)` when last claim drops); `build-reference/app-extracted/.vite/build/index.js:241909` (settings binding: `PHe = "keepAwakeEnabled"`); `build-reference/app-extracted/.vite/build/index.js:241914` (`vy.on("keepAwakeEnabled", YHe)` — toggle observer)
|
||||
|
||||
## S21 — Lid-close still suspends per OS policy
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Suspend inhibitor scope
|
||||
**Applies to:** All rows (laptop hosts)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. With "Keep computer awake" ON, close the laptop lid.
|
||||
2. Observe whether the machine suspends.
|
||||
|
||||
**Expected:** Machine still suspends per logind's `HandleLidSwitch=suspend`. The inhibit lock taken in [S20](#s20--keep-computer-awake-inhibits-idle-suspend) targets `idle:sleep`, not `handle-lid-switch`, so lid-close behavior is unaffected.
|
||||
|
||||
**Diagnostics on failure:** `loginctl show-session --property=HandleLidSwitch`, `journalctl --since="-5 minutes"`, the actual `--what=` flags on the Claude-owned inhibitor.
|
||||
|
||||
**References:** [How scheduled tasks run](https://code.claude.com/docs/en/desktop-scheduled-tasks#how-scheduled-tasks-run)
|
||||
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:241897` (only `"prevent-app-suspension"` is passed to `powerSaveBlocker.start` — Electron maps this to `idle:sleep`); no `handle-lid-switch` / `HandleLidSwitch` token anywhere in `index.js` (verified via `grep -nE 'lid|HandleLidSwitch|handle-lid' index.js`)
|
||||
@@ -0,0 +1,363 @@
|
||||
# Shortcuts & Input
|
||||
|
||||
Tests covering URL handling, the Quick Entry global shortcut, and DE-specific shortcut/input failure modes. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T05 — `claude://` URL handler opens links in-app
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** URL handler / xdg-open
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. With Claude Desktop running, in another app run `xdg-open 'claude://chat/new?q=hello'` (or click a `claude://` link in a browser/terminal).
|
||||
2. Observe.
|
||||
|
||||
**Expected:** Link is delivered to the running Claude Desktop process — no new browser tab, no crash, no error dialog. (Upstream's `claudeURLHandler` only accepts the `claude:`, `claude-dev:`, `claude-nest:`, `claude-nest-dev:`, `claude-nest-prod:` schemes; bare `https://claude.ai/...` clicks route through the user's default browser, not Claude Desktop. The `.desktop` file registers `MimeType=x-scheme-handler/claude` only, matching the upstream contract.)
|
||||
|
||||
**Diagnostics on failure:** `xdg-mime query default x-scheme-handler/claude`, the registered `.desktop` file content, launcher log, app crash report (if any), `coredumpctl list claude-desktop` (if subprocess died — see [S06](#s06--url-handler-doesnt-segfault-on-native-wayland)).
|
||||
|
||||
**References:** upstream `index.js:495996-496009` (`bEe()` protocol filter), `index.js:524819` (`setAsDefaultProtocolClient("claude")`), `index.js:525140-525148` (macOS `open-url`), `index.js:525162-525172` (Linux/Win `second-instance` argv path), project `scripts/packaging/{deb,rpm,appimage}.sh` (MimeType registration).
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:495996, 524819, 525140, 525162
|
||||
|
||||
## T06 — Quick Entry global shortcut (unfocused)
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Global shortcut / Electron globalShortcut
|
||||
**Applies to:** All rows
|
||||
**Issues:** [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393), [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406), [PR #102](https://github.com/aaddrick/claude-desktop-debian/pull/102), [PR #153](https://github.com/aaddrick/claude-desktop-debian/pull/153)
|
||||
|
||||
**Steps:**
|
||||
1. Launch app, focus another application (browser, terminal).
|
||||
2. Press the configured Quick Entry shortcut (default `Ctrl+Alt+Space`).
|
||||
3. Type a prompt and submit.
|
||||
4. Repeat from a different virtual desktop / workspace.
|
||||
|
||||
**Expected:** Quick Entry prompt opens regardless of focused app or workspace. Shortcut is globally registered, not focus-bound. Submitting creates a new session and shows it in the main window.
|
||||
|
||||
**Diagnostics on failure:** Launcher log (look for `Using X11 backend via XWayland (for global hotkey support)` or portal-shortcut markers), `XDG_SESSION_TYPE`, `XDG_CURRENT_DESKTOP`, output of `gdbus call --session --dest=org.freedesktop.portal.Desktop --object-path=/org/freedesktop/portal/desktop --method=org.freedesktop.DBus.Introspectable.Introspect`, the active patch set in `scripts/patches/`.
|
||||
|
||||
**References:** [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393), [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406)
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:499376 (`ort` default accelerator: `"Ctrl+Alt+Space"` non-mac, `"Alt+Space"` on mac), 499416 (`globalShortcut.register`), 525287-525290 (Quick Entry trigger callback registered against `Pw.QUICK_ENTRY`).
|
||||
|
||||
## S06 — URL handler doesn't segfault on native Wayland
|
||||
|
||||
**Severity:** Critical (for wlroots rows)
|
||||
**Surface:** URL handler subprocess
|
||||
**Applies to:** Sway, Niri, Hypr-O, Hypr-N (any native-Wayland session)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch the app on a native Wayland session (no XWayland forcing).
|
||||
2. From another app, click a `claude.ai` link or run `xdg-open https://claude.ai/...`.
|
||||
|
||||
**Expected:** Link opens in-app cleanly. No `Failed to connect to Wayland display` errors followed by a SIGSEGV from the URL handler subprocess.
|
||||
|
||||
**Diagnostics on failure:** `coredumpctl info claude-desktop`, `WAYLAND_DISPLAY` env in the subprocess (if capturable via `strace -f -e execve`), launcher log, full env dump.
|
||||
|
||||
**Currently:** Sway capture shows `Failed to connect to Wayland display: No such file or directory (2)` followed by `Segmentation fault` from the URL handler subprocess. The main app process keeps running; the URL handler dies. Not yet filed.
|
||||
|
||||
**References:** —
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:495996 (`bEe()` URL handler), 525140-525148 (`open-url` macOS), 525162-525172 (`second-instance` argv path on Linux); project `scripts/launcher-common.sh:96-99` (`--ozone-platform=x11` default), `scripts/launcher-common.sh:41-44` (Niri force-native-Wayland).
|
||||
|
||||
## S07 — `CLAUDE_USE_WAYLAND=1` opt-in path works without crashing
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Native Wayland mode
|
||||
**Applies to:** Sway, Niri, Hypr-O, Hypr-N
|
||||
**Issues:** [PR #228](https://github.com/aaddrick/claude-desktop-debian/pull/228), [PR #232](https://github.com/aaddrick/claude-desktop-debian/pull/232)
|
||||
|
||||
**Steps:**
|
||||
1. Set `CLAUDE_USE_WAYLAND=1`. Launch the app.
|
||||
2. Use the app for ~5 minutes — open chats, switch tabs, exercise basic flows.
|
||||
|
||||
**Expected:** App forces native Wayland (no XWayland), continues to render and respond. Previously broken paths in PR #228 still hold.
|
||||
|
||||
**Diagnostics on failure:** Launcher log (confirm Wayland mode active), `--doctor`, full env dump, screenshot of any crash dialog.
|
||||
|
||||
**References:** [PR #228](https://github.com/aaddrick/claude-desktop-debian/pull/228), [PR #232](https://github.com/aaddrick/claude-desktop-debian/pull/232)
|
||||
**Code anchors:** project `scripts/launcher-common.sh:28-29` (`CLAUDE_USE_WAYLAND=1` opt-out of XWayland), 100-111 (native-Wayland Electron flags: `UseOzonePlatform,WaylandWindowDecorations`, `--ozone-platform=wayland`, `--enable-wayland-ime`, `--wayland-text-input-version=3`, `GDK_BACKEND=wayland`).
|
||||
|
||||
## S09 — Quick window patch runs only on KDE (post-#406 gate)
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Patch gate
|
||||
**Applies to:** All rows (verifies the gate, not the feature)
|
||||
**Issues:** [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406), [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393)
|
||||
|
||||
**Steps:**
|
||||
1. On a KDE row, launch the app. Inspect launcher log for quick-window-patch markers.
|
||||
2. On a non-KDE row, launch the app. Inspect launcher log — the markers should be absent.
|
||||
|
||||
**Expected:** On KDE sessions the quick-window patch is applied (Quick Entry uses the patched code path). On non-KDE sessions the patch is **not** applied, preventing the [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393) regression on GNOME etc.
|
||||
|
||||
**Diagnostics on failure:** Launcher log, `XDG_CURRENT_DESKTOP`, the patch-gate code path in `scripts/patches/`.
|
||||
|
||||
**References:** [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406), [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393)
|
||||
**Code anchors:** project `scripts/patches/quick-window.sh:32-42` (KDE-gated `blur()` insertion), 115-125 (KDE-gated focus/visibility check replacement); upstream sites the patch rewrites are around `index.js:515374-515471` (Quick Entry popup construction + handlers).
|
||||
|
||||
## S10 — Quick Entry popup is transparent (no opaque square frame)
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Quick Entry window (KDE Wayland)
|
||||
**Applies to:** KDE-W
|
||||
**Issues:** [#370](https://github.com/aaddrick/claude-desktop-debian/issues/370), [#223](https://github.com/aaddrick/claude-desktop-debian/issues/223), [PR #244](https://github.com/aaddrick/claude-desktop-debian/pull/244)
|
||||
|
||||
**Steps:**
|
||||
1. On KDE Plasma Wayland, invoke Quick Entry.
|
||||
2. Observe the popup background.
|
||||
|
||||
**Expected:** Quick Entry popup renders with a transparent background — no opaque square frame visible behind the rounded prompt UI.
|
||||
|
||||
**Diagnostics on failure:** Screenshot, KDE compositor settings (`kwriteconfig5 --read kwinrc Compositing/Backend`), launcher log, BrowserWindow construction args.
|
||||
|
||||
**References:** [#370](https://github.com/aaddrick/claude-desktop-debian/issues/370) (current open report), [#223](https://github.com/aaddrick/claude-desktop-debian/issues/223) (closed predecessor), [PR #244](https://github.com/aaddrick/claude-desktop-debian/pull/244)
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515380 (`transparent: !0`), 515383 (`backgroundColor: "#00000000"`), 515381 (`frame: !1`), 515377 (`skipTaskbar: !0`).
|
||||
|
||||
## S11 — Quick Entry shortcut fires from any focus on Wayland (mutter XWayland key-grab)
|
||||
|
||||
**Severity:** Critical (for GNOME users)
|
||||
**Surface:** Global shortcut on GNOME mutter
|
||||
**Applies to:** GNOME, Ubu
|
||||
**Issues:** [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406)
|
||||
|
||||
**Steps:**
|
||||
1. On GNOME/mutter Wayland, launch the app.
|
||||
2. Focus another application; press the Quick Entry shortcut.
|
||||
3. Repeat from another virtual desktop.
|
||||
|
||||
**Expected:** Shortcut fires regardless of focused app or workspace.
|
||||
|
||||
**Diagnostics on failure:** Launcher log (note `Using X11 backend via XWayland (for global hotkey support)`), `XDG_CURRENT_DESKTOP`, mutter version (`gnome-shell --version`), the active patch set.
|
||||
|
||||
**Currently:** Fedora 43 GNOME Wayland reproduces [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404) on the default (XWayland) path — mutter doesn't honour the XWayland-side key grab, so the shortcut is focus-bound. The fix is opt-in: launch with `CLAUDE_USE_WAYLAND=1` to use native Wayland + the XDG GlobalShortcuts portal (see [S12](#s12----enable-featuresglobalshortcutsportal-launcher-flag-wired-up-for-gnome-wayland)), which mutter honours on **GNOME ≤ 49**. GNOME Wayland is not auto-flipped (rendering risk; GNOME 50 portal route is a no-op upstream). Re-verify on a GNOME Wayland host.
|
||||
|
||||
**References:** [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406)
|
||||
**Code anchors:** project `scripts/launcher-common.sh` `detect_display_backend` (native Wayland opt-in via `CLAUDE_USE_WAYLAND=1`; only Niri auto-forced) and `build_electron_args` (native-Wayland `GlobalShortcutsPortal` feature); upstream `index.js:499416` (`globalShortcut.register`).
|
||||
|
||||
## S12 — `--enable-features=GlobalShortcutsPortal` launcher flag wired up for GNOME Wayland
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Launcher flag wiring
|
||||
**Applies to:** GNOME, Ubu (any GNOME Wayland)
|
||||
**Issues:** [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404)
|
||||
|
||||
**Steps:**
|
||||
1. On GNOME Wayland, launch the app with `CLAUDE_USE_WAYLAND=1`.
|
||||
2. Inspect the Electron command line via `pgrep -af claude-desktop` — look for `--enable-features=GlobalShortcutsPortal`.
|
||||
3. Test Quick Entry shortcut from unfocused state (see [T06](#t06--quick-entry-global-shortcut-unfocused)).
|
||||
|
||||
**Expected:** With `CLAUDE_USE_WAYLAND=1`, the launcher uses native Wayland and emits `GlobalShortcutsPortal` inside a single merged `--enable-features=…` switch, routing global shortcuts through XDG Desktop Portal instead of X11 key grabs ([#404](https://github.com/aaddrick/claude-desktop-debian/issues/404); GNOME is not auto-flipped — the portal route is opt-in). Note the flag is comma-joined with `UseOzonePlatform,WaylandWindowDecorations`, so match the `GlobalShortcutsPortal` subkey, not an exact `--enable-features=GlobalShortcutsPortal` token.
|
||||
|
||||
**Diagnostics on failure:** Full process argv (`cat /proc/$(pgrep -f 'app\.asar')/cmdline | tr '\0' ' '`), launcher log (expect `Using native Wayland backend (global shortcuts via XDG portal)`), `XDG_CURRENT_DESKTOP`.
|
||||
|
||||
**Currently:** Launcher side implemented — `build_electron_args` adds `GlobalShortcutsPortal` to the native-Wayland feature set (opt-in via `CLAUDE_USE_WAYLAND=1`; GNOME is not auto-flipped). The flag is verified present in argv on that opt-in path (this case launches with `CLAUDE_USE_WAYLAND=1` and passes). Functional global-from-unfocused works on **GNOME ≤ 49** (first registration shows a one-time portal permission dialog). On **GNOME 50 / xdg-desktop-portal ≥ 1.20** it does not yet fire: Electron/Chromium never performs the portal's host `Registry.Register` app-id handshake, so `globalShortcut.register()` returns `false` and the portal is never contacted. Proven via D-Bus capture + a Python portal client; filed upstream as [electron/electron#51875](https://github.com/electron/electron/issues/51875).
|
||||
|
||||
**References:** [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404)
|
||||
**Code anchors:** project `scripts/launcher-common.sh` `detect_display_backend` (tri-state `CLAUDE_USE_WAYLAND` override) + `build_electron_args` (merged `enable_features` array). See [`wayland-global-shortcuts-portal.md`](../../learnings/wayland-global-shortcuts-portal.md).
|
||||
|
||||
## S14 — Global shortcuts via XDG portal work on Niri
|
||||
|
||||
**Severity:** Critical (for Niri users)
|
||||
**Surface:** XDG Desktop Portal `BindShortcuts`
|
||||
**Applies to:** Niri
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. On Niri, launch the app (the launcher special-cases Niri to native Wayland + portal).
|
||||
2. Configure the Quick Entry shortcut.
|
||||
3. Observe portal interaction in launcher log.
|
||||
|
||||
**Expected:** `BindShortcuts` succeeds. Configured Quick Entry shortcut is registered and fires.
|
||||
|
||||
**Diagnostics on failure:** Launcher log capture of the `BindShortcuts` call, `busctl --user tree org.freedesktop.portal.Desktop`, Niri version, full env.
|
||||
|
||||
**Currently:** `Failed to call BindShortcuts (error code 5)` — portal global shortcuts fail on Niri. Different root cause from [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), same user-visible symptom (Quick Entry shortcut doesn't fire). Not yet filed.
|
||||
|
||||
**References:** —
|
||||
**Code anchors:** project `scripts/launcher-common.sh` `detect_display_backend` (Niri force-native-Wayland branch) + `build_electron_args` (native-Wayland `GlobalShortcutsPortal` feature, which Niri now also receives); upstream `index.js:499416` (`globalShortcut.register`, which on native Wayland routes through Electron's `xdg-desktop-portal` `BindShortcuts` path inside Chromium). wlroots' portal ships no GlobalShortcuts backend, so `BindShortcuts` still fails until that lands — this stays a known-failing detector.
|
||||
|
||||
## S29 — Quick Entry popup is created lazily on first shortcut press (closed-to-tray sanity)
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Quick Entry popup lifecycle
|
||||
**Applies to:** All rows
|
||||
**Issues:** [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393)
|
||||
|
||||
**Steps:**
|
||||
1. Launch app, wait for main window to appear, hide-to-tray (close via X — see [T08](./tray-and-window-chrome.md#t08--hide-to-tray-on-close)).
|
||||
2. Confirm no Claude window is mapped (e.g. `wmctrl -l | grep -i claude` returns empty on X11; `swaymsg -t get_tree` for Wayland equivalents).
|
||||
3. Press the Quick Entry shortcut.
|
||||
4. Type `hello`, press Enter.
|
||||
|
||||
**Expected:** Popup appears even though no Claude window was mapped before the keypress. Upstream constructs the popup `BrowserWindow` lazily on first shortcut invocation (`if (!Ko || ...) Ko = new BrowserWindow(...)` near `index.js:515375`), so the popup does not need a pre-existing main window. New chat session is created and reachable on submit.
|
||||
|
||||
**Diagnostics on failure:** Launcher log, `~/.config/Claude/logs/`, `XDG_CURRENT_DESKTOP`, screenshot of empty desktop after shortcut press.
|
||||
|
||||
**References:** [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393), upstream `index.js:515375-515397`
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515374 (`if (!Ko ...) Ko = new BrowserWindow(...)` lazy construction guard), 515394 (`preload: ".vite/build/quickWindow.js"`), 515438 (`Ko.loadFile(".vite/renderer/quick_window/quick-window.html")`).
|
||||
|
||||
## S30 — Quick Entry shortcut becomes a no-op after full app exit
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Global shortcut unregistration
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch app. Confirm Quick Entry shortcut works (popup opens).
|
||||
2. Quit Claude Desktop fully via tray → Quit (or `pkill -f app.asar`). Confirm no `electron` processes for the app remain.
|
||||
3. Press the Quick Entry shortcut.
|
||||
|
||||
**Expected:** No popup appears. No error dialog. No zombie process. Electron unregisters the global shortcut on app exit; the shortcut becomes a system-level no-op.
|
||||
|
||||
**Diagnostics on failure:** `pgrep -af app.asar` output, `journalctl --user -e -n 100`, OS-level shortcut bindings (`gsettings list-recursively | grep -i shortcut`).
|
||||
|
||||
**References:** upstream `index.js:499416` (registration site)
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:499398-499428 (`nG()` register/unregister wrapper — passing `null` accelerator unregisters), 499416 (`hA.globalShortcut.register`), 499403 (`hA.globalShortcut.unregister`).
|
||||
|
||||
## S31 — Quick Entry submit makes the new chat reachable from any main-window state
|
||||
|
||||
**Severity:** Critical
|
||||
**Surface:** Submit → main window show
|
||||
**Applies to:** All rows
|
||||
**Issues:** [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393), [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406)
|
||||
|
||||
**Steps:**
|
||||
1. For each main-window state: (a) visible-and-focused, (b) minimized, (c) hidden-to-tray, (d) on a different workspace, (e) closed via X (project's hide-to-tray override).
|
||||
2. Set the state, then invoke Quick Entry, type `hello`, submit.
|
||||
3. Record what happens to the main window: auto-restored, requires tray click, came to current workspace, stayed on its own workspace.
|
||||
|
||||
**Expected:** The new chat session is **reachable** from each starting state. Acceptance is "user can reach the new chat" — not "main window auto-restored." Upstream calls `mainWin.show()` + `mainWin.focus()` only (`index.js:515566, 515599`), with no `restore()`, no `setVisibleOnAllWorkspaces()`, no `moveTop()`. Whether `show()` un-minimizes or migrates workspaces is purely compositor-dependent. The failure case is "new chat created but the user has no way to surface it" — that's a regression. Anything that reaches the chat (even via a tray click) is upstream-acceptable.
|
||||
|
||||
**Diagnostics on failure:** `~/.config/Claude/logs/`, screenshot at each state, output of `wmctrl -l` (X11) or `swaymsg -t get_tree` (sway), launcher log.
|
||||
|
||||
**Currently:** On non-KDE rows, the post-#406 KDE-only patch gate leaves the upstream code path (`isFocused()` short-circuit) active. Andrej730's #393 GNOME repro shows the stale-`isFocused()` bug can still suppress `show()` in tray-only state. See [S32](#s32--quick-entry-submit-on-gnome-mutter-doesnt-trip-electron-stale-isfocused).
|
||||
|
||||
**References:** [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393), upstream `index.js:515566, 515599, 105164-171`
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515567 (`h1() || ut.show(), ut.focus()` in `gHn()` existing-chat path), 515598-515599 (`h1() || ut.show(), ut.focus()` in `ynt()` new-chat path), 105164-105171 (`h1()` returns `ut.isFocused() || mainView.webContents.isFocused()`).
|
||||
|
||||
## S32 — Quick Entry submit on GNOME mutter doesn't trip Electron stale-`isFocused()`
|
||||
|
||||
**Severity:** Critical (for GNOME users)
|
||||
**Surface:** Electron `BrowserWindow.isFocused()` on Linux
|
||||
**Applies to:** GNOME, Ubu
|
||||
**Issues:** [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393)
|
||||
|
||||
**Steps:**
|
||||
1. On GNOME Wayland, launch the app, then close to tray.
|
||||
2. Confirm the app is in tray-only state (no window mapped, no Dash entry, no taskbar entry).
|
||||
3. Invoke Quick Entry, type `hello`, submit.
|
||||
4. Repeat after re-pinning the app to the Dash and reproducing the tray-only state from there.
|
||||
|
||||
**Expected:** Submit produces a reachable new chat session in both Dash-pinned and not-pinned cases. **The Dash distinction is empirical, not code-driven** — upstream has no notion of Dash presence. The underlying failure mode is Electron's `BrowserWindow.isFocused()` returning stale-true on Linux mutter, which causes upstream's `h1() || ut.show()` short-circuit (`index.js:515566`) to skip `show()`. Andrej730 traced this on #393.
|
||||
|
||||
**Diagnostics on failure:** Bundled `index.js` h1() body (extract via `npx asar extract`); add temporary logging in `h1()` per Andrej730's diff in #393 if reproducing locally; `gnome-shell --version`; `~/.config/Claude/logs/`.
|
||||
|
||||
**Currently:** Open. The KDE-only gate from PR #406 leaves this path unfixed on GNOME. Resolution requires either (a) widening the patch to all DEs by dropping the `isFocused()` fallback in the patched code, or (b) waiting for an upstream Electron fix to `isFocused()` on Linux.
|
||||
|
||||
**References:** [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393) (Andrej730's diagnosis with `eU()` logging output)
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:105164-105171 (`h1()` body — the exact short-circuit Andrej730 instrumented), 515567 + 515598 (the two `h1() || ut.show()` call sites the suppression hits).
|
||||
|
||||
## S33 — Quick Entry transparent rendering tracked against bundled Electron version
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Bundled Electron version
|
||||
**Applies to:** All rows (relevant where #370 reproduces)
|
||||
**Issues:** [#370](https://github.com/aaddrick/claude-desktop-debian/issues/370)
|
||||
|
||||
**Steps:**
|
||||
1. After install, capture the Electron version bundled with the app: extract `app.asar.unpacked` and run the bundled Electron with `--version`, or read it from the bundled binary's metadata.
|
||||
2. Record the version in [`../matrix.md`](../matrix.md) per row, alongside the [S10](#s10--quick-entry-popup-is-transparent-no-opaque-square-frame) status.
|
||||
|
||||
**Expected:** Captured version is recorded. If the version is **41.0.4 through 41.x.y** and S10 fails, the upstream electron/electron#50213 regression hypothesis (per @noctuum's bisect on #370) holds and the issue is blocked on upstream. If the version is **41.0.3 or earlier** and S10 fails, the bisect is wrong — investigate. If the version is **a later release that includes a CSD-rendering fix** and S10 still fails, the upstream-regression hypothesis is also wrong.
|
||||
|
||||
**Diagnostics on failure:** Output of the version capture command, link to electron/electron#50213, the BrowserWindow construction args from the bundled `index.js`.
|
||||
|
||||
**Currently:** Per @noctuum's bisect, 41.0.4 introduced the regression. No upstream fix shipped as of last check.
|
||||
|
||||
**References:** [#370](https://github.com/aaddrick/claude-desktop-debian/issues/370), upstream `index.js:515380, 515383` (already sets `transparent: true` and `backgroundColor: "#00000000"`)
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515380 (`transparent: !0`), 515383 (`backgroundColor: "#00000000"`), 515374-515397 (popup `BrowserWindow` construction args block, including `frame: !1`, `hasShadow: Zr`, `type: Zr ? "panel" : void 0`).
|
||||
|
||||
## S34 — Quick Entry shortcut focuses fullscreen main window instead of showing popup
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Shortcut behavior on fullscreen main
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch app. Put the main window into native fullscreen (F11 or platform equivalent).
|
||||
2. Press the Quick Entry shortcut.
|
||||
|
||||
**Expected:** Popup does **not** appear. Main window receives focus and `ide()` runs (upstream behavior at `index.js:525287-525290`). This is intentional upstream UX — assumes the user wants to interact with the existing fullscreen Claude rather than overlay a popup on it.
|
||||
|
||||
**Diagnostics on failure:** Screenshot, launcher log, confirm fullscreen state via `wmctrl -l -G` / Wayland equivalent.
|
||||
|
||||
**References:** upstream `index.js:525287-525290`
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:525287-525290 (Quick Entry callback: `ut && !ut.isDestroyed() && ut.isFullScreen() ? (ut.focus(), ide()) : Yri()`), 515234-515241 (`ide()` — `show()` + `focus()` + `webContents.send(TEe.cmdK)` for the cmd-K dispatch).
|
||||
|
||||
## S35 — Quick Entry popup position is persisted across invocations and across app restarts
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Popup placement memory
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch app. Invoke Quick Entry. Note the popup position (record monitor + coordinates if possible — e.g. `xdotool getactivewindow getwindowgeometry` on X11).
|
||||
2. Dismiss (Esc). Re-invoke. Position should be unchanged across this dismiss/re-invoke cycle.
|
||||
3. Quit Claude Desktop fully (`pkill -f app.asar`). Re-launch. Invoke Quick Entry.
|
||||
4. Confirm position matches the pre-restart capture.
|
||||
|
||||
**Expected:** Popup reappears at the same monitor + position before and after a full app restart. Upstream persists position via `an.get("quickWindowPosition")` (`index.js:515491-515526`), keyed on monitor label + resolution.
|
||||
|
||||
**Diagnostics on failure:** Captured coordinates pre/post-restart, content of any persisted settings file (project's settings storage location varies by OS).
|
||||
|
||||
**References:** upstream `index.js:515491-515526`
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515444-515461 (`Ko.on("hide", …)` persists `quickWindowPosition` via `an.set(...)`), 515491-515521 (`aHn()` resolves saved monitor by `label + bounds.width + bounds.height`, falling back to label-only or proportional placement), 515489 (`Ko.setPosition(...)` after show).
|
||||
|
||||
## S36 — Quick Entry popup falls back to primary display when saved monitor is gone
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Multi-monitor placement
|
||||
**Applies to:** All rows with a multi-monitor capable host
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. **Multi-monitor required.** With an external monitor connected, invoke Quick Entry on the external monitor. Trigger position persistence (per [S35](#s35--quick-entry-popup-position-is-persisted-across-invocations-and-across-app-restarts)).
|
||||
2. Disconnect the external monitor (libvirt: detach the second display device; bare metal: unplug).
|
||||
3. Invoke Quick Entry.
|
||||
|
||||
**Expected:** Popup appears on the primary display, not at off-screen coordinates. Upstream falls back to `cHn()` when the saved monitor is no longer present (`index.js:515502`).
|
||||
|
||||
**Diagnostics on failure:** `xrandr` (X11) / `wlr-randr` (wlroots) output before and after disconnect, captured popup coordinates, screenshot.
|
||||
|
||||
**Skip when:** Single-monitor VM or host. Skip with `-` in the dashboard.
|
||||
|
||||
**References:** upstream `index.js:515502`
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515502 (`return cHn();` early-return when no saved position), 515523-515527 (`cHn()` centres popup on `screen.getPrimaryDisplay()` workArea), 515514-515515 (`label`-only match fallback before primary-display fallback).
|
||||
|
||||
## S37 — Quick Entry popup remains functional after main window destroy
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Popup lifecycle independence from main window
|
||||
**Applies to:** All rows (where reachable)
|
||||
**Issues:** —
|
||||
|
||||
**Steps:**
|
||||
1. Launch app, focus main window.
|
||||
2. **Trigger main window destroy without quitting the app.** On this project, the X-button hide-to-tray override means the standard close path does **not** destroy `ut`. Reach the destroy path via one of:
|
||||
- DevTools console on the main window: `require('electron').remote.getCurrentWindow().destroy()` (if `remote` is exposed; not guaranteed).
|
||||
- A debug build with the hide-to-tray override removed.
|
||||
- Skip and mark `-` if unreachable.
|
||||
3. After destroy: invoke Quick Entry, type `hello`, submit.
|
||||
|
||||
**Expected:** Popup appears and accepts input. Upstream's `!ut || ut.isDestroyed()` guard at `index.js:515595` skips the show/focus block without crashing. The new chat is created in the data layer; whether it has a window to surface in is a separate question (upstream contract is "popup itself does not crash").
|
||||
|
||||
**Diagnostics on failure:** Crash dump, `~/.config/Claude/logs/`, sequence of actions taken to reach the destroy path.
|
||||
|
||||
**Currently:** Likely unreachable on Linux without a debug build, due to project's hide-to-tray override of the X button. Mark `-` (N/A) on rows where the destroy path can't be triggered.
|
||||
|
||||
**References:** upstream `index.js:515595`
|
||||
**Code anchors:** build-reference/app-extracted/.vite/build/index.js:515595-515602 (`setTimeout(() => { !ut || ut.isDestroyed() || (h1() || ut.show(), ut.focus(), Qe == null || Qe.webContents.focus(), iri()); }, 0)` — guard skips show/focus block on destroy without throwing); 515547 (companion guard in `nde()` chat-id submit path: `else if (ut && !ut.isDestroyed())`).
|
||||
@@ -0,0 +1,123 @@
|
||||
# Tray & Window Chrome
|
||||
|
||||
Tests covering the tray icon, OS-native window decorations, the hybrid in-app topbar (PR #538), and hide-to-tray on close. See [`../matrix.md`](../matrix.md) for status.
|
||||
|
||||
## T03 — Tray icon present
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** System tray / SNI
|
||||
**Applies to:** All rows
|
||||
**Issues:** —
|
||||
**Runner:** [`tools/test-harness/src/runners/T03_tray_icon_present.spec.ts`](../../../tools/test-harness/src/runners/T03_tray_icon_present.spec.ts) — registration only (left-click toggle + theme-switch in-place rebuild are v2)
|
||||
|
||||
**Steps:**
|
||||
1. Launch the app. Wait a few seconds.
|
||||
2. Locate the tray icon in the system tray / status area.
|
||||
3. Right-click → confirm standard menu (Show, Quit, etc.). Left-click → confirm window toggles.
|
||||
4. Switch the system theme between light and dark; observe the tray icon update.
|
||||
|
||||
**Expected:** Tray icon appears within a few seconds of app launch. Right-click exposes the standard menu. Left-click toggles main window visibility. Theme changes update the icon in place without spawning a duplicate.
|
||||
|
||||
**Diagnostics on failure:** `RegisteredStatusNotifierItems` from the SNI watcher (see [runbook](../runbook.md#tray--dbus-state-kde)), the tray daemon process for the DE (Plasma's `plasmashell`, GNOME's `gnome-shell` + AppIndicator extension state, etc.), launcher log.
|
||||
|
||||
**References:** [`docs/learnings/tray-rebuild-race.md`](../../learnings/tray-rebuild-race.md)
|
||||
**Code anchors:** `build-reference/app-extracted/.vite/build/index.js:525627` (`vy.on("menuBarEnabled", () => { Sde() })` — re-entry), `index.js:525631-525673` (`function Sde()` — tray construction), `index.js:525645` (`new hA.Tray(hA.nativeImage.createFromPath(t))`), `index.js:525646` (`qh.on("click", () => void Yri())` — left-click handler), `index.js:525653` (`qh.setContextMenu(mnt())` — Linux right-click via context menu), `index.js:515150-515169` (`function mnt()` — Show App + Quit menu items), `index.js:525623` (`hA.nativeTheme.on("updated", ...)` — theme-change re-entry).
|
||||
|
||||
## T04 — Window decorations draw
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Window chrome
|
||||
**Applies to:** All rows
|
||||
**Issues:** [PR #127](https://github.com/aaddrick/claude-desktop-debian/pull/127), [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538)
|
||||
**Runner:** [`tools/test-harness/src/runners/T04_window_decorations.spec.ts`](../../../tools/test-harness/src/runners/T04_window_decorations.spec.ts) — X11 / XWayland only (checks `_NET_FRAME_EXTENTS`); native-Wayland window-state queries are deferred
|
||||
|
||||
**Steps:**
|
||||
1. Launch the app.
|
||||
2. Confirm window has a working OS-native frame: close, minimize, maximize render and respond.
|
||||
3. Resize via window edges.
|
||||
|
||||
**Expected:** Frame is drawn by the DE/compositor (not the app). All controls render and respond. Resize works.
|
||||
|
||||
**Diagnostics on failure:** `xprop _NET_WM_WINDOW_TYPE` (X11) / `swaymsg -t get_tree` or compositor-equivalent (Wayland), launcher log line for `frame:` setting, screenshot.
|
||||
|
||||
**References:** [PR #127](https://github.com/aaddrick/claude-desktop-debian/pull/127), [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538) (hybrid mode keeps native frame), [`docs/learnings/linux-topbar-shim.md`](../../learnings/linux-topbar-shim.md)
|
||||
**Code anchors:** Upstream factory passes `titleBarStyle: "hidden"` and `titleBarOverlay: ys` (Windows-only flag) to `BrowserWindow` at `build-reference/app-extracted/.vite/build/index.js:524892-524909` (`Ori()`). On Linux the wrapper at `scripts/frame-fix-wrapper.js:122` overrides to `options.frame = true` and at `scripts/frame-fix-wrapper.js:129-130` deletes the macOS-only `titleBarStyle` / `titleBarOverlay` so the DE draws the frame. (Hybrid-mode plumbing — `CLAUDE_TITLEBAR_STYLE` resolution and the `native`/`hybrid`/`hidden` branches — lives on `main` per PR #538; the docs/compat-matrix branch's `frame-fix-wrapper.js` carries only the unconditional `frame:true` patch, which is sufficient for T04's "frame draws" assertion.)
|
||||
|
||||
## T07 — In-app topbar renders + clickable
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** In-app topbar (hybrid mode)
|
||||
**Applies to:** All rows on PR #538 builds
|
||||
**Issues:** [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538), [PR #127](https://github.com/aaddrick/claude-desktop-debian/pull/127)
|
||||
|
||||
**Steps:**
|
||||
1. Launch a PR #538 build.
|
||||
2. Observe the in-app topbar below the OS frame.
|
||||
3. Click each of: hamburger menu, sidebar toggle, search, back, forward, Cowork ghost.
|
||||
|
||||
**Expected:** All five topbar buttons render below the native frame. Each responds to mouse clicks (no implicit drag region capturing the events). If any single button fails to render or click, the test is `✗` — note which one in the linked issue.
|
||||
|
||||
**Diagnostics on failure:** Screenshot, env (`OZONE_PLATFORM`, `ELECTRON_OZONE_PLATFORM_HINT`, `GDK_BACKEND`, `QT_QPA_PLATFORM`, `MOZ_ENABLE_WAYLAND`, `SDL_VIDEODRIVER`), launcher log, DevTools `document.querySelector('.topbar')` HTML if accessible.
|
||||
|
||||
**References:** [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538), [PR #127](https://github.com/aaddrick/claude-desktop-debian/pull/127), [`docs/learnings/linux-topbar-shim.md`](../../learnings/linux-topbar-shim.md)
|
||||
**Code anchors:** UA-spoof shim source `scripts/wco-shim.js` (lines 1-30 module guard / `CLAUDE_TITLEBAR_STYLE != 'native'` gate, lines 184-191 `navigator.userAgent` redefinition matching `/(win32|win64|windows|wince)/i`, lines 52-53 `CONTROLS_WIDTH=140` / `TITLEBAR_HEIGHT=40`); injection orchestrator `scripts/patches/wco-shim.sh` (`patch_wco_shim()` prepends shim source to `mainView.js`); hybrid-mode wrapper branch `scripts/frame-fix-wrapper.js:62-70` (`VALID_TITLEBAR_STYLES`, default `hybrid`) and `:152-240` (per-mode `frame` / `titleBarStyle` handling).
|
||||
|
||||
## T08 — Hide-to-tray on close
|
||||
|
||||
**Severity:** Smoke
|
||||
**Surface:** Window lifecycle
|
||||
**Applies to:** All rows
|
||||
**Issues:** [PR #451](https://github.com/aaddrick/claude-desktop-debian/pull/451)
|
||||
|
||||
**Steps:**
|
||||
1. Launch the app. Click the window close (X) button.
|
||||
2. Confirm app process is still running (`pgrep -af claude-desktop`).
|
||||
3. Click the tray icon (or invoke Quick Entry) → window restores.
|
||||
4. Quit explicitly via tray menu or `Ctrl+Q`.
|
||||
|
||||
**Expected:** Close button hides main window to tray, doesn't quit. App keeps running. Tray-click restores. Explicit Quit ends the process.
|
||||
|
||||
**Diagnostics on failure:** `pgrep -af claude-desktop` after close, launcher log, screenshot of any dialog.
|
||||
|
||||
**References:** [PR #451](https://github.com/aaddrick/claude-desktop-debian/pull/451)
|
||||
**Code anchors:** Upstream Linux quit-on-last-close at `build-reference/app-extracted/.vite/build/index.js:525550-525552` (`hA.app.on("window-all-closed", () => { Zr || Ap() })` — `Zr` is darwin). Wrapper interception at `scripts/frame-fix-wrapper.js:178-185` (`this.on('close', e => { if (!result.app._quittingIntentionally && !this.isDestroyed()) { e.preventDefault(); this.hide() } })`) and `scripts/frame-fix-wrapper.js:370-374` (`app.on('before-quit', () => { app._quittingIntentionally = true })` — arms the bypass for tray-Quit / `Ctrl+Q` / SIGTERM). `CLOSE_TO_TRAY` gate (Linux + `CLAUDE_QUIT_ON_CLOSE !== '1'`) at `scripts/frame-fix-wrapper.js:49-51`. Tray Quit menu item `mnt()` `click: rde` at `index.js:515166`; `function rde()` at `index.js:515306-515308` calls `Ap(!1)`.
|
||||
|
||||
## S08 — Tray icon doesn't duplicate after `nativeTheme` update
|
||||
|
||||
**Severity:** Should
|
||||
**Surface:** Tray (KDE)
|
||||
**Applies to:** KDE-W, KDE-X
|
||||
**Issues:** [`docs/learnings/tray-rebuild-race.md`](../../learnings/tray-rebuild-race.md)
|
||||
|
||||
**Steps:**
|
||||
1. Launch the app on KDE.
|
||||
2. Toggle system theme (light ↔ dark).
|
||||
3. Observe the tray for ~10 seconds.
|
||||
|
||||
**Expected:** Tray icon updates in place via `setImage` + `setContextMenu`. SNI service stays registered — no de-register / re-register churn that would leave a duplicate icon visible until KDE garbage-collects.
|
||||
|
||||
**Diagnostics on failure:** SNI watcher state before/after theme switch (see [runbook](../runbook.md#tray--dbus-state-kde)), launcher log, `journalctl --user -u plasma-plasmashell -n 50`.
|
||||
|
||||
**References:** [`docs/learnings/tray-rebuild-race.md`](../../learnings/tray-rebuild-race.md). Mitigated upstream — the in-place fast-path is the current behavior.
|
||||
**Code anchors:** Upstream destroy+recreate slow-path at `build-reference/app-extracted/.vite/build/index.js:525643` (`qh && (qh.destroy(), (qh = null))`) followed immediately by `new hA.Tray(...)` at `:525645` and `setContextMenu(mnt())` at `:525653` — the SNI re-register that races on KDE. Fast-path injection in `scripts/patches/tray.sh` `patch_tray_inplace_update()` (lines 95-231): extracts `tray_var` / `menu_func` / `path_var` / `enabled_var` dynamically, then injects `if (TRAY && ENABLED !== false) { TRAY.setImage(EL.nativeImage.createFromPath(PATH)); process.platform !== "darwin" && TRAY.setContextMenu(MENU()); return }` before the destroy block. Idempotency marker at `tray.sh:174-180` keys on the post-rename `setImage(...nativeImage.createFromPath(PATH_VAR))` literal. Mutex + 250 ms DBus settle delay (the prior mitigation, kept for the legitimate slow-path entries) at `tray.sh:48-60`.
|
||||
|
||||
## S13 — Hybrid topbar shim survives Omarchy's Ozone-Wayland env exports
|
||||
|
||||
**Severity:** Critical (for Omarchy users)
|
||||
**Surface:** In-app topbar (hybrid mode) under Omarchy env
|
||||
**Applies to:** Hypr-O
|
||||
**Issues:** [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538)
|
||||
|
||||
**Steps:**
|
||||
1. On OmarchyOS, export Omarchy's session-wide env (`ELECTRON_OZONE_PLATFORM_HINT=wayland`, `OZONE_PLATFORM=wayland`, `GDK_BACKEND=wayland,x11,*`, `QT_QPA_PLATFORM=wayland;xcb`, `MOZ_ENABLE_WAYLAND=1`, `SDL_VIDEODRIVER=wayland,x11`).
|
||||
2. Launch a PR #538 build.
|
||||
3. Click each of the five topbar buttons.
|
||||
|
||||
**Expected:** The hybrid-mode topbar shim (`scripts/wco-shim.js`) loads in time to spoof the UA before claude.ai's `isWindows()` check fires. All five topbar buttons render and click.
|
||||
|
||||
**Diagnostics on failure:** Full session env, launcher log, `--doctor`, screenshot, video (per @lukedev45's bug report on PR #538), DevTools console for shim-load errors.
|
||||
|
||||
**Currently:** Reproduces partial render on OmarchyOS Hyprland per [@lukedev45](https://github.com/lukedev45)'s video on [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538). @aaddrick attempted local repro on KDE Plasma + Wayland with the same env vars and could not reproduce; root cause TBD pending diagnostic capture from a broken run.
|
||||
|
||||
**References:** [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538), [`docs/learnings/linux-topbar-shim.md`](../../learnings/linux-topbar-shim.md)
|
||||
**Code anchors:** Shim is inlined at the top of `mainView.js` (the BrowserView preload), not loaded via `require` — see the rationale at `scripts/patches/wco-shim.sh:23-40` ("Sandboxed preloads can only require a fixed allowlist of modules…"). The injection prepends `scripts/wco-shim.js` source at the start of `app.asar.contents/.vite/build/mainView.js` so the UA override fires before the bundle's `isWindows()` regex (`/(win32|win64|windows|wince)/i`) ever runs in the page main world (`scripts/wco-shim.js:184-191`). The shim's IIFE no-ops on non-Linux at `wco-shim.js:29` and on `CLAUDE_TITLEBAR_STYLE === 'native'` at `wco-shim.js:30-32`, so the only env-export interaction with `OZONE_PLATFORM` etc. is via Chromium's own platform plumbing — none of those exports are read by the shim itself, which makes the partial-render repro on Omarchy mysterious to static analysis.
|
||||
@@ -0,0 +1,179 @@
|
||||
# Test Status Matrix
|
||||
|
||||
*Last updated: 2026-04-30 · Tested against: claude-desktop 1.4758.0 (project varies per row)*
|
||||
|
||||
This is the live dashboard. Update this file (and only this file) when status changes. For the test specs themselves, see [`cases/`](./cases/). For orientation, see [`README.md`](./README.md).
|
||||
|
||||
Status legend: `✓` pass · `✗` fail · `🔧` mitigated · `?` untested · `-` N/A. Cells include linked issue/PR numbers when relevant.
|
||||
|
||||
## Cross-environment matrix (T-series)
|
||||
|
||||
| Test | KDE-W | KDE-X | GNOME | Ubu | Sway | i3 | Niri | Hypr-O | Hypr-N |
|
||||
|------|-------|-------|-------|-----|------|----|------|--------|--------|
|
||||
| [T01](./cases/launch.md#t01--app-launch) | ✓ | ? | ? | ? | ? | ? | ? | ? | ✓ |
|
||||
| [T02](./cases/launch.md#t02--doctor-health-check) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T03](./cases/tray-and-window-chrome.md#t03--tray-icon-present) | ✓ | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T04](./cases/tray-and-window-chrome.md#t04--window-decorations-draw) | ✓ | ? | ? | ? | ? | ? | ? | ? | ✓ |
|
||||
| [T05](./cases/shortcuts-and-input.md#t05--url-handler-opens-claudeai-links-in-app) | ? | ? | ? | ? | ✗ | ? | ? | ? | ? |
|
||||
| [T06](./cases/shortcuts-and-input.md#t06--quick-entry-global-shortcut-unfocused) | ✓ | ✓ | ✗ [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404) | 🔧 [#406](https://github.com/aaddrick/claude-desktop-debian/pull/406) | ? | ? | ✗ | ? | ? |
|
||||
| [T07](./cases/tray-and-window-chrome.md#t07--in-app-topbar-renders--clickable) | ? | ? | ? | ? | ? | ? | ? | ✗ [#538](https://github.com/aaddrick/claude-desktop-debian/pull/538) | ✓ |
|
||||
| [T08](./cases/tray-and-window-chrome.md#t08--hide-to-tray-on-close) | ✓ | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T09](./cases/platform-integration.md#t09--autostart-via-xdg) | ✓ | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T10](./cases/platform-integration.md#t10--cowork-integration) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T11](./cases/extensibility.md#t11--plugin-install-anthropic--partners) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T12](./cases/platform-integration.md#t12--webgl-warn-only) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T13](./cases/launch.md#t13--doctor-reports-correct-package-format) | ✗ | ✗ | ✗ | ? | ✗ | ✗ | ✗ | ? | ? |
|
||||
| [T14](./cases/launch.md#t14--multi-instance-behavior) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T15](./cases/code-tab-foundations.md#t15--sign-in-completes-via-browser-handoff) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T16](./cases/code-tab-foundations.md#t16--code-tab-loads) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T17](./cases/code-tab-foundations.md#t17--folder-picker-opens) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T18](./cases/code-tab-foundations.md#t18--drag-and-drop-files-into-prompt) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T19](./cases/code-tab-foundations.md#t19--integrated-terminal) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T20](./cases/code-tab-foundations.md#t20--file-pane-opens-and-saves) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T21](./cases/code-tab-workflow.md#t21--dev-server-preview-pane) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T22](./cases/code-tab-workflow.md#t22--pr-monitoring-via-gh) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T23](./cases/code-tab-handoff.md#t23--desktop-notifications-fire) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T24](./cases/code-tab-handoff.md#t24--open-in-external-editor) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T25](./cases/code-tab-handoff.md#t25--show-in-files-file-manager) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T26](./cases/routines.md#t26--routines-page-renders) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T27](./cases/routines.md#t27--scheduled-task-fires-and-notifies) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T28](./cases/routines.md#t28--scheduled-task-catch-up-after-suspend) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T29](./cases/code-tab-workflow.md#t29--worktree-isolation) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T30](./cases/code-tab-workflow.md#t30--auto-archive-on-pr-merge) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T31](./cases/code-tab-workflow.md#t31--side-chat-opens) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T32](./cases/code-tab-workflow.md#t32--slash-command-menu) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T33](./cases/extensibility.md#t33--plugin-browser) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T34](./cases/code-tab-handoff.md#t34--connector-oauth-round-trip) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T35](./cases/extensibility.md#t35--mcp-server-config-picked-up) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T36](./cases/extensibility.md#t36--hooks-fire) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T37](./cases/extensibility.md#t37--claudemd-memory-loads) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T38](./cases/code-tab-handoff.md#t38--continue-in-ide) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
| [T39](./cases/code-tab-handoff.md#t39--desktop-cli-handoff-graceful-na) | ? | ? | ? | ? | ? | ? | ? | ? | ? |
|
||||
|
||||
## Environment-specific status
|
||||
|
||||
### Ubuntu / DEB
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S01](./cases/distribution.md#s01--appimage-launches-without-manual-libfuse2t64-install) | AppImage launches without manual `libfuse2t64` install | ✗ | Workaround documented; not yet filed |
|
||||
| [S02](./cases/distribution.md#s02--xdg_current_desktopubuntu-gnome-doesnt-break-de-detection) | `XDG_CURRENT_DESKTOP=ubuntu:GNOME` doesn't break DE detection | ? | — |
|
||||
| [S03](./cases/distribution.md#s03--deb-install-via-apt-pulls-all-required-runtime-deps) | DEB install via APT pulls all required runtime deps | ? | — |
|
||||
|
||||
### Fedora / RPM
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S04](./cases/distribution.md#s04--rpm-install-via-dnf-pulls-all-required-runtime-deps) | RPM install via DNF pulls all required runtime deps | ? | — |
|
||||
| [S05](./cases/distribution.md#s05--doctor-recognises-dnf-installed-package-doesnt-false-flag-as-appimage) | Doctor recognises dnf-installed package (no AppImage false-flag) | ✗ | Affects KDE-W, KDE-X, GNOME, Sway, i3, Niri (T13) |
|
||||
|
||||
### Wayland-native (wlroots)
|
||||
|
||||
Applies to: Sway, Niri, Hypr-O, Hypr-N (any session running native Wayland rather than XWayland).
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S06](./cases/shortcuts-and-input.md#s06--url-handler-doesnt-segfault-on-native-wayland) | URL handler doesn't segfault on native Wayland | ✗ on Sway | Captured; not yet filed |
|
||||
| [S07](./cases/shortcuts-and-input.md#s07--claude_use_wayland1-opt-in-path-works-without-crashing) | `CLAUDE_USE_WAYLAND=1` opt-in path works | ? | [#228](https://github.com/aaddrick/claude-desktop-debian/pull/228), [#232](https://github.com/aaddrick/claude-desktop-debian/pull/232) |
|
||||
|
||||
### KDE
|
||||
|
||||
Applies to: KDE-W, KDE-X.
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S08](./cases/tray-and-window-chrome.md#s08--tray-icon-doesnt-duplicate-after-nativetheme-update) | Tray icon doesn't duplicate after `nativeTheme` update | 🔧 | [`tray-rebuild-race.md`](../learnings/tray-rebuild-race.md) |
|
||||
| [S09](./cases/shortcuts-and-input.md#s09--quick-window-patch-runs-only-on-kde-post-406-gate) | Quick window patch runs only on KDE | ✓ | [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406) |
|
||||
| [S10](./cases/shortcuts-and-input.md#s10--quick-entry-popup-is-transparent-no-opaque-square-frame) | Quick Entry popup is transparent | ? | [#370](https://github.com/aaddrick/claude-desktop-debian/issues/370), [#223](https://github.com/aaddrick/claude-desktop-debian/issues/223) |
|
||||
|
||||
### GNOME
|
||||
|
||||
Applies to: GNOME, Ubu (Ubuntu's GNOME), and any other mutter session.
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S11](./cases/shortcuts-and-input.md#s11--quick-entry-shortcut-fires-from-any-focus-on-wayland-mutter-xwayland-key-grab) | Quick Entry shortcut fires from any focus | ✗ on GNOME, 🔧 on Ubu | [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), [PR #406](https://github.com/aaddrick/claude-desktop-debian/pull/406) |
|
||||
| [S12](./cases/shortcuts-and-input.md#s12----enable-featuresglobalshortcutsportal-launcher-flag-wired-up-for-gnome-wayland) | `--enable-features=GlobalShortcutsPortal` wired up | ? | [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404) |
|
||||
|
||||
### Omarchy
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S13](./cases/tray-and-window-chrome.md#s13--hybrid-topbar-shim-survives-omarchys-ozone-wayland-env-exports) | Hybrid topbar shim survives Omarchy's Ozone-Wayland env exports | ✗ | [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538) |
|
||||
|
||||
### Niri
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S14](./cases/shortcuts-and-input.md#s14--global-shortcuts-via-xdg-portal-work-on-niri) | Global shortcuts via XDG portal work on Niri | ✗ | Captured; not yet filed |
|
||||
|
||||
### AppImage
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S15](./cases/distribution.md#s15--appimage-extraction---appimage-extract-works-as-documented-fallback) | AppImage extraction (`--appimage-extract`) works as fallback | ? | — |
|
||||
| [S16](./cases/distribution.md#s16--appimage-mount-cleans-up-on-app-exit) | AppImage mount cleans up on app exit | ? | — |
|
||||
|
||||
### Linux launcher / `.desktop` env handling
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S17](./cases/platform-integration.md#s17--app-launched-from-desktop-inherits-shell-path) | App launched from `.desktop` inherits shell `PATH` | ? | — |
|
||||
| [S18](./cases/platform-integration.md#s18--local-environment-editor-persists-across-reboot) | Local environment editor persists across reboot | ? | — |
|
||||
| [S19](./cases/routines.md#s19--claude_config_dir-redirects-scheduled-task-storage) | `CLAUDE_CONFIG_DIR` redirects scheduled-task storage | ? | — |
|
||||
|
||||
### Idle-sleep / suspend
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S20](./cases/routines.md#s20--keep-computer-awake-inhibits-idle-suspend) | "Keep computer awake" inhibits idle suspend | ? | — |
|
||||
| [S21](./cases/routines.md#s21--lid-close-still-suspends-per-os-policy) | Lid-close still suspends per OS policy | ? | — |
|
||||
|
||||
### Computer Use (Linux: out-of-scope per upstream)
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S22](./cases/platform-integration.md#s22--computer-use-toggle-is-absent-or-visibly-disabled-on-linux) | Computer-use toggle is absent or visibly disabled | ? | — |
|
||||
| [S23](./cases/platform-integration.md#s23--dispatch-spawned-sessions-dont-soft-lock-on-a-never-approvable-computer-use-prompt) | Dispatch sessions don't soft-lock on never-approvable prompt | ? | — |
|
||||
|
||||
### Dispatch
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S24](./cases/platform-integration.md#s24--dispatch-spawned-code-session-appears-with-badge-and-notification) | Dispatch-spawned Code session appears with badge + notification | ? | — |
|
||||
| [S25](./cases/platform-integration.md#s25--mobile-pairing-survives-linux-session-restart) | Mobile pairing survives Linux session restart | ? | — |
|
||||
|
||||
### Auto-update vs. system package manager
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S26](./cases/distribution.md#s26--auto-update-is-disabled-when-installed-via-apt--dnf) | Auto-update is disabled when installed via `apt` / `dnf` | ? | — |
|
||||
|
||||
### Plugin / worktree storage
|
||||
|
||||
| ID | Test | Status | Notes |
|
||||
|----|------|--------|-------|
|
||||
| [S27](./cases/extensibility.md#s27--plugins-install-per-user-not-into-system-paths) | Plugins install per-user, not into system paths | ? | — |
|
||||
| [S28](./cases/extensibility.md#s28--worktree-creation-surfaces-clear-error-on-read-only-mounts) | Worktree creation surfaces clear error on read-only mounts | ? | — |
|
||||
|
||||
## Known failures rollup
|
||||
|
||||
Tests currently `✗` somewhere — investigation priority order:
|
||||
|
||||
| Test | Failing on | Root cause |
|
||||
|------|------------|------------|
|
||||
| [T05 / S06](./cases/shortcuts-and-input.md#s06--url-handler-doesnt-segfault-on-native-wayland) | Sway | URL handler subprocess SIGSEGV on native Wayland — `Failed to connect to Wayland display` |
|
||||
| [T06 / S11](./cases/shortcuts-and-input.md#s11--quick-entry-shortcut-fires-from-any-focus-on-wayland-mutter-xwayland-key-grab) | GNOME | mutter doesn't honour XWayland-side key grab |
|
||||
| [T06 / S14](./cases/shortcuts-and-input.md#s14--global-shortcuts-via-xdg-portal-work-on-niri) | Niri | `BindShortcuts` returns error code 5 |
|
||||
| [T07 / S13](./cases/tray-and-window-chrome.md#s13--hybrid-topbar-shim-survives-omarchys-ozone-wayland-env-exports) | Hypr-O | Hybrid topbar shim partial render under Omarchy's Ozone-Wayland env exports |
|
||||
| [T13 / S05](./cases/launch.md#t13--doctor-reports-correct-package-format) | every Fedora row | Doctor only checks dpkg, false-flags every dnf install as AppImage |
|
||||
| [S01](./cases/distribution.md#s01--appimage-launches-without-manual-libfuse2t64-install) | Ubuntu 24.04 | AppImage requires `libfuse2t64`; not auto-pulled |
|
||||
|
||||
## Notes on the current state
|
||||
|
||||
- Most cells are `?` because every captured VM in the recent test session ran the **released** build (`dnf install` / `apt install` / current AppImage), which predates [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538). Topbar verification (T07) on the VM rows specifically requires a branch build deployed before any cell can flip from `?`.
|
||||
- KDE-W status reflects @aaddrick's daily-driver host (Nobara KDE Plasma Wayland) where multiple features have been in continuous use.
|
||||
- Hypr-N status reflects @typedrat's report on [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538) ("Working great on NixOS with Hyprland").
|
||||
- Hypr-O status reflects @lukedev45's broken-case report on [PR #538](https://github.com/aaddrick/claude-desktop-debian/pull/538) (partial render, root cause unconfirmed but Omarchy-env-specific — see [S13](./cases/tray-and-window-chrome.md#s13--hybrid-topbar-shim-survives-omarchys-ozone-wayland-env-exports)).
|
||||
- T13 is `✗` on every Fedora row because the dpkg false-flag is a deterministic property of the doctor script, not a per-environment failure mode. It will flip to `✓` everywhere once the doctor learns to detect rpm/dnf installs.
|
||||
- T15–T39 are derived from upstream Claude Code Desktop docs (`code.claude.com/docs/en/desktop*`) — features whose Linux behavior is officially undocumented (the docs explicitly state "Linux is not supported" for the Code tab). All cells start as `?` because the upstream Code-tab feature surface has not been systematically exercised on the patched Linux build.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Quick Entry — Upstream Contract + Test Index
|
||||
|
||||
Reference doc for the Quick Entry surface. Two halves:
|
||||
|
||||
- [§ Upstream design intent](#upstream-design-intent) documents what upstream Quick Entry promises vs. doesn't, with code anchors into `build-reference/app-extracted/.vite/build/index.js`. Treat as the authoritative answer when triaging whether a Quick Entry behavior is a Linux compat regression (our problem) or upstream-by-design (not our problem).
|
||||
- [§ Test list](#test-list) enumerates the QE-N items as conceptual checks and maps each to the concrete S-N / T-N case that backs it. Spec headnotes (S09, S12, S31, S37) cite specific QE-N IDs by anchor; [§ Scaffold integration](#scaffold-integration) is the authoritative QE-N → S-N table.
|
||||
|
||||
The QE-N items originated in the close-out sweep for [#393](https://github.com/aaddrick/claude-desktop-debian/issues/393), [#404](https://github.com/aaddrick/claude-desktop-debian/issues/404), and [#370](https://github.com/aaddrick/claude-desktop-debian/issues/370). The sweep has run; what remains is the upstream-contract reference + the test-index mapping.
|
||||
|
||||
## Upstream design intent
|
||||
|
||||
Read this before reading the test list. Several `QE-*` rows test things upstream does not actually promise — those tests are still valuable as black-box behavior checks, but the calibration of "expected" matters.
|
||||
|
||||
Source for everything below: `build-reference/app-extracted/.vite/build/index.js`. Symbol names (`h1`, `ut`, `Ko`, `ynt`, `nde`, `g3A`, `u7A`) drift between releases — anchor on shape, not name.
|
||||
|
||||
### What upstream promises
|
||||
|
||||
- **Global shortcut** registered via Electron `globalShortcut.register()` (`:499416`). No app-focus gate — fires regardless of which app is focused.
|
||||
- **Popup is lazily created** on first shortcut press (`if (!Ko || ...) Ko = new BrowserWindow(...)` near `:515375`). The popup `BrowserWindow` is constructed on demand, not at app startup. This is what makes QE-4 (closed-to-tray) work.
|
||||
- **Position memory:** popup position persists across invocations via `an.get("quickWindowPosition")` (`:515491-515526`), keyed on monitor label + resolution. If the original monitor is gone, falls back to primary display.
|
||||
- **Submit always creates a NEW chat session** when no `chatId` is provided (`ynt(e)` at `:515546`). Quick Entry never appends to an existing conversation.
|
||||
- **Click-outside dismiss** is wired in the main process via the popup `blur` handler (`Ko.on("blur", () => g3A(null))` at `:515465`).
|
||||
- **Popup survives main-window close.** If the user closes the main window via the X button (not full quit), `!ut || ut.isDestroyed()` guards at `:515595` skip the `show()/focus()` calls; the popup itself remains functional.
|
||||
- **Window construction** sets `transparent: true`, `backgroundColor: "#00000000"`, `frame: false`, `alwaysOnTop: true` (level `"pop-up-menu"`), `skipTaskbar: true`, `resizable: false`, `show: false` (`:515375-515397`). `hasShadow: Zr` and `type: Zr ? "panel" : void 0` are macOS-only (`Zr === process.platform === "darwin"`).
|
||||
|
||||
### What upstream does NOT promise
|
||||
|
||||
- **Workspace migration.** No `setVisibleOnAllWorkspaces()`, no `moveTop()`, no `setWorkspace()` is called anywhere in the Quick Entry submit path. Whether the main window comes to the user's current workspace or stays on its own is purely a compositor decision driven by `mainWin.show()` + `mainWin.focus()`. **Linux/Wayland behavior here is not part of the upstream feature spec.**
|
||||
- **Restore from minimized.** No `restore()` call in the submit path. `show()` un-minimizes on most WMs; whether it does on a given Wayland compositor is up to that compositor.
|
||||
- **Multi-monitor placement on cursor / focused display.** Upstream uses last-saved position or primary display, never "where the user is right now."
|
||||
- **Multi-window targeting.** All `show`/`focus` calls go through `ut` (the main window). If the user has multiple windows, behavior is undefined.
|
||||
- **Popup re-creation if its `BrowserWindow` is destroyed.** Upstream does not re-construct `Ko` after destroy — it's only created on first shortcut press.
|
||||
- **Compositor-aware behavior.** Upstream has no concept of "GNOME vs KDE vs wlroots." Anywhere our patches branch on `XDG_CURRENT_DESKTOP`, that's our project compensating for compositor-specific Electron breakage, not implementing an upstream-defined contract.
|
||||
|
||||
### Edge case: fullscreen main window
|
||||
|
||||
`:525287-525290` reads (paraphrased): *"if `ut` exists and `ut.isFullScreen()` is true, focus `ut` and call `ide()`; else show the Quick Entry popup."* So if the main window is fullscreen when the shortcut fires, **the popup does not appear** — the shortcut focuses the main window instead. QE-1 needs this caveat.
|
||||
|
||||
### Edge case: `h1()` is a *don't-show-if-already-focused* optimization
|
||||
|
||||
The visibility-check function (`h1()` at `:105164-105171`) is upstream's mechanism for "don't redundantly call `show()` if the main window is already focused." Sound design. The reason it's broken on Linux is Electron's `BrowserWindow.isFocused()` returning stale-true after `hide()` on Linux backends — i.e., **the patch we apply is fixing a Linux-Electron bug, not diverging from upstream intent.** Once `isFocused()` returns honest values on Linux, the patch could be retired.
|
||||
|
||||
## Test list
|
||||
|
||||
Each item is a single check. Severity tier matches the existing scaffolding (Critical / Should / Smoke). Existing test ID in parentheses — `(new)` means this item should be added to [`cases/shortcuts-and-input.md`](./cases/shortcuts-and-input.md) before this sweep is reproducible by anyone else.
|
||||
|
||||
### Shortcut activation — covers #404
|
||||
|
||||
| ID | Severity | Step | Expected | Existing |
|
||||
|----|----------|------|----------|----------|
|
||||
| QE-1 | Smoke | App focused (not fullscreen), press shortcut | Popup appears. **Edge case from upstream design:** if main window is fullscreen, the shortcut focuses main and runs `ide()` instead of showing the popup (`:525287-525290`). Test this fullscreen variant separately as QE-1b — popup should *not* appear. | [S34](./cases/shortcuts-and-input.md#s34--quick-entry-shortcut-focuses-fullscreen-main-window-instead-of-showing-popup) (QE-1b only) |
|
||||
| QE-2 | Critical | Other app focused, press shortcut | Popup appears | [T06](./cases/shortcuts-and-input.md#t06--quick-entry-global-shortcut-unfocused), [S11](./cases/shortcuts-and-input.md#s11--quick-entry-shortcut-fires-from-any-focus-on-wayland-mutter-xwayland-key-grab) |
|
||||
| QE-3 | Critical | App on a different workspace, press shortcut | Popup appears on current workspace | [T06](./cases/shortcuts-and-input.md#t06--quick-entry-global-shortcut-unfocused) |
|
||||
| QE-4 | Critical | App closed-to-tray (no window mapped), press shortcut | Popup appears | [S29](./cases/shortcuts-and-input.md#s29--quick-entry-popup-is-created-lazily-on-first-shortcut-press-closed-to-tray-sanity) |
|
||||
| QE-5 | Should | App quit entirely, press shortcut | No popup, no error, no zombie process | [S30](./cases/shortcuts-and-input.md#s30--quick-entry-shortcut-becomes-a-no-op-after-full-app-exit) |
|
||||
| QE-6 | Should | Inspect Electron argv via `cat /proc/$(pgrep -f 'app\.asar')/cmdline \| tr '\0' ' '` (the launcher script also matches `claude-desktop`, so anchor on `app.asar` to hit the Electron process). Cross-check launcher log line `Using X11 backend via XWayland (for global hotkey support)` (the GNOME default) vs `Using native Wayland backend (global shortcuts via XDG portal)` (after `CLAUDE_USE_WAYLAND=1`). | **Launcher implemented (S12).** GNOME defaults to XWayland (no portal flag); launching with `CLAUDE_USE_WAYLAND=1` adds `--ozone-platform=wayland` and a single `--enable-features=…,GlobalShortcutsPortal` (comma-joined with the ozone features, not a standalone token). On that opt-in path QE-2 / QE-3 pass on **GNOME ≤ 49** after the one-time portal dialog; on **GNOME 50 / xdg-desktop-portal ≥ 1.20** they don't yet — Electron skips the portal's `Registry.Register` handshake ([electron#51875](https://github.com/electron/electron/issues/51875)). | [S12](./cases/shortcuts-and-input.md#s12----enable-featuresglobalshortcutsportal-launcher-flag-wired-up-for-gnome-wayland) |
|
||||
|
||||
### Submit → main window — covers #393
|
||||
|
||||
| ID | Severity | Step | Expected | Existing |
|
||||
|----|----------|------|----------|----------|
|
||||
| QE-7 | Smoke | Main window visible, submit prompt from QE | Popup closes; main window navigates to a **new** chat session (not appended to current chat — `ynt(e)` at `:515546` always creates new). | [S31](./cases/shortcuts-and-input.md#s31--quick-entry-submit-makes-the-new-chat-reachable-from-any-main-window-state) |
|
||||
| QE-8 | Critical | Main window minimized, submit | **Upstream calls `show() + focus()` only — no `restore()`.** Whether the WM un-minimizes is compositor-dependent. Test as black-box: record whether the new chat is reachable to the user (window comes back to view, OR user has to click tray/dock to see it). Both outcomes are upstream-acceptable; only "new chat created but unreachable" is a regression. | [S31](./cases/shortcuts-and-input.md#s31--quick-entry-submit-makes-the-new-chat-reachable-from-any-main-window-state) |
|
||||
| QE-9 | Critical | Main window hidden-to-tray (after [T08](./cases/tray-and-window-chrome.md#t08--hide-to-tray-on-close)), submit | Same as QE-8 — `show()` should re-map a hidden window on most compositors, but upstream doesn't guarantee it. The new chat must be reachable; the path to reach it (auto vs tray-click) is compositor-dependent. | [S31](./cases/shortcuts-and-input.md#s31--quick-entry-submit-makes-the-new-chat-reachable-from-any-main-window-state) |
|
||||
| QE-10 | Should | Main window on different workspace, submit | **Upstream has no workspace logic** (no `setVisibleOnAllWorkspaces`, no `moveTop`). Outcome is whatever the compositor decides on `show()` + `focus()`. Record observed behavior per row; do not treat any single outcome as the "right" one. | [S31](./cases/shortcuts-and-input.md#s31--quick-entry-submit-makes-the-new-chat-reachable-from-any-main-window-state) |
|
||||
| QE-11 | Critical | **GNOME-specific (Andrej730 repro):** App in tray, *not* present in Dash/dock, submit | Main window opens. The codebase doesn't reason about Dash presence — this is purely a compositor-observed state. The underlying failure is `BrowserWindow.isFocused()` returning stale-true on GNOME mutter, which causes the patched (KDE) code path's `h1() || ut.show()` chain to short-circuit before `show()`. Test as a black-box repro. | [S32](./cases/shortcuts-and-input.md#s32--quick-entry-submit-on-gnome-mutter-doesnt-trip-electron-stale-isfocused) |
|
||||
| QE-12 | Should | App in tray, *also* present in Dash/dock, submit | Main window opens (this state should not trip the stale-focus bug, but verify) | [S32](./cases/shortcuts-and-input.md#s32--quick-entry-submit-on-gnome-mutter-doesnt-trip-electron-stale-isfocused) |
|
||||
| QE-13 | Smoke | Submit prompt with 1-2 chars (`hi`) | Upstream silently drops. The actual gate is `> 2` chars at `index.js:515530, 515533` — anything 3+ submits. So `hi` (2) drops, `hel` (3) submits. Document, do not fix. | — |
|
||||
|
||||
### Visual / window appearance — covers #370
|
||||
|
||||
| ID | Severity | Step | Expected | Existing |
|
||||
|----|----------|------|----------|----------|
|
||||
| QE-14 | Should | Inspect popup background | Transparent; no opaque square frame visible behind the rounded UI. **Note:** upstream already sets `transparent: true` and `backgroundColor: "#00000000"` (`:515380, :515383`), so the #370 triage-bot suggestion to "try setting backgroundColor to transparent" is moot — those are already in place. The Electron 41.0.4 regression is at the CSD/shadow rendering layer below those flags, not at the option-passing layer. | [S10](./cases/shortcuts-and-input.md#s10--quick-entry-popup-is-transparent-no-opaque-square-frame) |
|
||||
| QE-15 | Smoke | Inspect popup chrome | No titlebar, no close/min/max buttons (frameless) | — |
|
||||
| QE-16 | Smoke | Inspect popup edges | Drop shadow + rounded corners render (compositor-dependent — note where missing) | — |
|
||||
| QE-17 | Smoke | Open popup, then click on another window | Popup stays above (always-on-top) | — |
|
||||
| QE-18 | Should | `electron --version` against the running app's bundled binary; record version in matrix | When > 41.0.4 ships and #370 still reproduces, the upstream-regression hypothesis is wrong | [S33](./cases/shortcuts-and-input.md#s33--quick-entry-transparent-rendering-tracked-against-bundled-electron-version) |
|
||||
|
||||
### Patch-application sanity — regression prevention
|
||||
|
||||
| ID | Severity | Step | Expected | Existing |
|
||||
|----|----------|------|----------|----------|
|
||||
| QE-19 | Critical | **All rows.** Extract the installed `app.asar` (`npx asar extract /usr/lib/claude-desktop-unofficial/resources/app.asar /tmp/inspect-installed`) and grep the bundled JS for the KDE gate string injected by the patch: `grep -c 'XDG_CURRENT_DESKTOP' /tmp/inspect-installed/.vite/build/index.js`. The patch (`scripts/patches/quick-window.sh:34-35, 117-118`) injects `(process.env.XDG_CURRENT_DESKTOP\|\|"").toLowerCase().includes("kde")` — that string is the runtime fingerprint. Note: the `Patched quick window` / `WARNING: No quick entry show() calls patched` lines from the patch are **build-time stdout** (not in `launcher.log`); check the build log if you built locally. | Bundled JS contains the KDE gate string (patch ran at build time). The patch ships in every build; the KDE-vs-non-KDE branch is decided at runtime by the env-var check. **Runtime gate effectiveness is verified implicitly by QE-7 through QE-12 passing on KDE and the unpatched-equivalent path running on non-KDE.** | [S09](./cases/shortcuts-and-input.md#s09--quick-window-patch-runs-only-on-kde-post-406-gate) |
|
||||
|
||||
### Input behavior smoke — catches collateral breakage
|
||||
|
||||
| ID | Severity | Step | Expected | Existing |
|
||||
|----|----------|------|----------|----------|
|
||||
| QE-21 | Smoke | In popup: `Esc` dismisses; click-outside dismisses; `Shift+Enter` inserts newline; `Enter` submits | All four behave as labelled. **Implementation notes for diagnostics:** click-outside is wired in the **main process** via the popup's `blur` handler (`:515465`). `Esc` / `Enter` / `Shift+Enter` are **renderer-side** (not visible in `index.js`); they go through IPC to `requestDismiss()` (`:515409`) and `requestDismissWithPayload()`. If a dismiss key fails, isolate which side is broken before reporting. | — |
|
||||
|
||||
### Popup placement & lifecycle — upstream contract sanity
|
||||
|
||||
These verify upstream-promised behaviors that aren't directly broken by #393/#404/#370 but live in the same surface area. Failures here would indicate a separate regression — file a new issue rather than folding it into the close-out trio.
|
||||
|
||||
| ID | Severity | Step | Expected | Existing |
|
||||
|----|----------|------|----------|----------|
|
||||
| QE-22 | Should | Invoke Quick Entry. Note popup position. Dismiss (Esc). Quit Claude Desktop entirely (`pkill -f app.asar` after closing the main window, or via tray → Quit). Re-launch. Invoke Quick Entry. | Popup reappears at the same monitor + position as before the restart. Upstream persists position via `an.get("quickWindowPosition")` (`:515491-515526`), keyed on monitor label + resolution. Position must survive a full app restart, not just dismiss/re-invoke. | [S35](./cases/shortcuts-and-input.md#s35--quick-entry-popup-position-is-persisted-across-invocations-and-across-app-restarts) |
|
||||
| QE-23 | Smoke | **Multi-monitor required.** With an external monitor connected, invoke Quick Entry on the external monitor — let the position be saved (trigger QE-22's persistence path). Disconnect the external monitor (libvirt: `virsh detach-device` for the second display, or unplug the host monitor passing through). Invoke Quick Entry. | Popup falls back to the primary display via `cHn()` (`:515502`). Does **not** appear at off-screen coordinates. Skip this row in single-monitor VMs. | [S36](./cases/shortcuts-and-input.md#s36--quick-entry-popup-falls-back-to-primary-display-when-saved-monitor-is-gone) |
|
||||
| QE-24 | Should | Launch app, focus main window, then **destroy** the main window without quitting the app. On this project the X button hide-to-tray override means the standard close path won't destroy `ut`; force the destroy via a) DevTools console (`Cmd+Opt+I` / `Ctrl+Shift+I` → `require('electron').remote.getCurrentWindow().destroy()` if exposed), or b) accept that this case is unreachable on Linux without a code change and skip. After destroy, invoke Quick Entry, type, submit. | Popup remains functional (lazy-recreation on shortcut press; the `!ut \|\| ut.isDestroyed()` guard at `:515595` skips the show/focus block but does not crash). New chat creation may not have a window to surface in — if app remains running with no main window, this is the "popup outlives main" path upstream guarantees. **If unreachable on Linux, mark this row N/A and document why.** | [S37](./cases/shortcuts-and-input.md#s37--quick-entry-popup-remains-functional-after-main-window-destroy) |
|
||||
|
||||
## Scaffold integration
|
||||
|
||||
The `QE-*` items in [§ Test list](#test-list) map onto formal `S##` test cases in [`cases/shortcuts-and-input.md`](./cases/shortcuts-and-input.md):
|
||||
|
||||
| Case | Title | Backs |
|
||||
|------|-------|-------|
|
||||
| [S29](./cases/shortcuts-and-input.md#s29--quick-entry-popup-is-created-lazily-on-first-shortcut-press-closed-to-tray-sanity) | Popup created lazily on first shortcut press (closed-to-tray sanity) | QE-4 |
|
||||
| [S30](./cases/shortcuts-and-input.md#s30--quick-entry-shortcut-becomes-a-no-op-after-full-app-exit) | Shortcut becomes no-op after full app exit | QE-5 |
|
||||
| [S31](./cases/shortcuts-and-input.md#s31--quick-entry-submit-makes-the-new-chat-reachable-from-any-main-window-state) | Submit makes the new chat reachable from any main-window state | QE-7 through QE-10 |
|
||||
| [S32](./cases/shortcuts-and-input.md#s32--quick-entry-submit-on-gnome-mutter-doesnt-trip-electron-stale-isfocused) | Submit on GNOME mutter doesn't trip Electron stale-`isFocused()` | QE-11, QE-12 |
|
||||
| [S33](./cases/shortcuts-and-input.md#s33--quick-entry-transparent-rendering-tracked-against-bundled-electron-version) | Transparent rendering tracked against bundled Electron version | QE-18 |
|
||||
| [S34](./cases/shortcuts-and-input.md#s34--quick-entry-shortcut-focuses-fullscreen-main-window-instead-of-showing-popup) | Shortcut focuses fullscreen main instead of showing popup | QE-1b |
|
||||
| [S35](./cases/shortcuts-and-input.md#s35--quick-entry-popup-position-is-persisted-across-invocations-and-across-app-restarts) | Popup position persisted across invocations and across app restarts | QE-22 |
|
||||
| [S36](./cases/shortcuts-and-input.md#s36--quick-entry-popup-falls-back-to-primary-display-when-saved-monitor-is-gone) | Popup falls back to primary display when saved monitor is gone | QE-23 |
|
||||
| [S37](./cases/shortcuts-and-input.md#s37--quick-entry-popup-remains-functional-after-main-window-destroy) | Popup remains functional after main window destroy | QE-24 |
|
||||
|
||||
QE-13, QE-15, QE-16, QE-17, and QE-21 are visual / input checks with no formal S-ID — run them by eye against [§ Upstream design intent](#upstream-design-intent).
|
||||
@@ -0,0 +1,340 @@
|
||||
# Testing Runbook
|
||||
|
||||
*Last updated: 2026-05-03*
|
||||
|
||||
How to run a test sweep, capture diagnostics, file failures, and update [`matrix.md`](./matrix.md). For the test specs themselves, see [`cases/`](./cases/). For the automation harness, see [`automation.md`](./automation.md) and [`tools/test-harness/`](../../tools/test-harness/). For the grounding sweep workflow (verify case docs against the live build), see [Grounding sweep](#grounding-sweep) below.
|
||||
|
||||
## When to sweep
|
||||
|
||||
| Trigger | Scope | Rows |
|
||||
|---------|-------|------|
|
||||
| Release tag (`vX.Y.Z+claude...`) | Smoke set | KDE-W + Hypr-N (or Sway) |
|
||||
| Release tag, monthly | Smoke + Critical | All active rows |
|
||||
| Upstream Claude Desktop bump | Smoke set + [grounding sweep](#grounding-sweep) | KDE-W + one wlroots row |
|
||||
| PR touching `scripts/patches/*.sh` | Tests in the affected surface (use surface tags in cases files) | KDE-W minimum |
|
||||
| Bug report citing an env | The relevant test on the reporter's row | Just that row |
|
||||
|
||||
## Setup: VM matrix
|
||||
|
||||
Each non-host row in [`matrix.md`](./matrix.md) is a QEMU/KVM guest. Standard config:
|
||||
|
||||
- 4 GB RAM, 2 vCPU minimum
|
||||
- virtio-gpu **with** `gl=on` (3D acceleration). On hybrid GPU hosts, pin `rendernode=/dev/dri/renderD129` (AMD); avoid renderD128 (NVIDIA, EGL init fails on aaddrick's laptop)
|
||||
- 32 GB qcow2 disk
|
||||
- Bridged networking
|
||||
- Virgil 3D enabled where possible (helps WebGL detection in T12)
|
||||
|
||||
ISOs / images per row:
|
||||
|
||||
| Row | Source |
|
||||
|-----|--------|
|
||||
| Fedora 43 (KDE-W, KDE-X, GNOME, Sway, i3, Niri) | https://fedoraproject.org/spins/ for KDE/GNOME, https://fedoraproject.org/sericea/ for Sway, manual install for i3/Niri |
|
||||
| Ubuntu 24.04 (Ubu) | https://ubuntu.com/download/desktop |
|
||||
| OmarchyOS (Hypr-O) | https://omarchy.org |
|
||||
| NixOS (Hypr-N) | https://nixos.org/download with Hyprland module |
|
||||
|
||||
For the host (KDE-W), test against Nobara directly — no VM needed.
|
||||
|
||||
## Setup: building the install candidate
|
||||
|
||||
```bash
|
||||
# Build from the branch under test
|
||||
./build.sh --build appimage --clean no
|
||||
./build.sh --build deb --clean no
|
||||
./build.sh --build rpm --clean no
|
||||
|
||||
# Or pull from CI artifacts for a tagged release
|
||||
gh run download <RUN_ID> -n package-amd64-deb
|
||||
gh run download <RUN_ID> -n package-amd64-rpm
|
||||
gh run download <RUN_ID> -n package-amd64-appimage
|
||||
```
|
||||
|
||||
Drop the resulting `.deb` / `.rpm` / `.AppImage` into a shared folder mounted into each guest, or `scp` per-guest.
|
||||
|
||||
## Running a sweep: the standard loop
|
||||
|
||||
For each test in scope:
|
||||
|
||||
1. **Read the test spec** in `cases/<surface>.md` (or `ui/<surface>.md` for UI checklists). Note the `Severity`, `Steps`, and `Expected` sections.
|
||||
2. **Execute the steps** as described.
|
||||
3. **Compare against Expected.** Mark internally as `✓`, `✗`, `🔧`, or `?` (untested if you couldn't run it for env reasons; `-` if N/A).
|
||||
4. **On `✗`**: capture the diagnostics from the test's `Diagnostics on failure` block (see [diagnostic capture](#diagnostic-capture) below). File an issue if one isn't already linked.
|
||||
5. **Update [`matrix.md`](./matrix.md)** in a single PR per row per sweep, titled `test: <ROW> sweep YYYY-MM-DD`.
|
||||
|
||||
## Diagnostic capture
|
||||
|
||||
Standard captures referenced from test `Diagnostics on failure` blocks:
|
||||
|
||||
### `--doctor` output
|
||||
|
||||
```bash
|
||||
claude-desktop-unofficial --doctor 2>&1 | tee /tmp/doctor.txt
|
||||
```
|
||||
|
||||
Or for AppImage:
|
||||
|
||||
```bash
|
||||
./claude-desktop-*.AppImage --doctor 2>&1 | tee /tmp/doctor.txt
|
||||
```
|
||||
|
||||
### Launcher log
|
||||
|
||||
```bash
|
||||
cat ~/.cache/claude-desktop-debian/launcher.log
|
||||
```
|
||||
|
||||
Truncate and re-run if the file is stale:
|
||||
|
||||
```bash
|
||||
: > ~/.cache/claude-desktop-debian/launcher.log
|
||||
claude-desktop 2>&1 | tee -a ~/.cache/claude-desktop-debian/launcher.log
|
||||
```
|
||||
|
||||
### Session env
|
||||
|
||||
```bash
|
||||
echo "XDG_SESSION_TYPE=$XDG_SESSION_TYPE"
|
||||
echo "XDG_CURRENT_DESKTOP=$XDG_CURRENT_DESKTOP"
|
||||
echo "WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
|
||||
echo "DISPLAY=$DISPLAY"
|
||||
echo "GDK_BACKEND=$GDK_BACKEND"
|
||||
echo "QT_QPA_PLATFORM=$QT_QPA_PLATFORM"
|
||||
echo "OZONE_PLATFORM=$OZONE_PLATFORM"
|
||||
echo "ELECTRON_OZONE_PLATFORM_HINT=$ELECTRON_OZONE_PLATFORM_HINT"
|
||||
```
|
||||
|
||||
### Tray / DBus state (KDE)
|
||||
|
||||
```bash
|
||||
# List registered tray icons
|
||||
gdbus call --session --dest=org.kde.StatusNotifierWatcher \
|
||||
--object-path=/StatusNotifierWatcher \
|
||||
--method=org.freedesktop.DBus.Properties.Get \
|
||||
org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems
|
||||
|
||||
# Find which process owns a connection
|
||||
gdbus call --session --dest=org.freedesktop.DBus \
|
||||
--object-path=/org/freedesktop/DBus \
|
||||
--method=org.freedesktop.DBus.GetConnectionUnixProcessID ":1.XXXX"
|
||||
```
|
||||
|
||||
### Portal availability (Wayland)
|
||||
|
||||
```bash
|
||||
systemctl --user status xdg-desktop-portal
|
||||
busctl --user tree org.freedesktop.portal.Desktop
|
||||
```
|
||||
|
||||
### Suspend inhibitors
|
||||
|
||||
```bash
|
||||
systemd-inhibit --list
|
||||
```
|
||||
|
||||
### App version
|
||||
|
||||
```bash
|
||||
claude-desktop-unofficial --version
|
||||
gh variable get CLAUDE_DESKTOP_VERSION
|
||||
gh variable get REPO_VERSION
|
||||
```
|
||||
|
||||
Always include the upstream version + project version in the issue body and the matrix-update commit message.
|
||||
|
||||
## Filing failures
|
||||
|
||||
Issue title format: `[<row>] <T## or S##>: <one-line symptom>`
|
||||
|
||||
Issue body template:
|
||||
|
||||
```markdown
|
||||
**Test:** [T17 — Folder picker opens](./docs/testing/cases/code-tab-foundations.md#t17--folder-picker-opens)
|
||||
**Environment:** GNOME (Fedora 43, Wayland)
|
||||
**Project version:** v1.3.23+claude1.4758.0
|
||||
**Upstream version:** 1.4758.0
|
||||
|
||||
## Steps
|
||||
<paste from test spec>
|
||||
|
||||
## Expected
|
||||
<paste from test spec>
|
||||
|
||||
## Actual
|
||||
<observed behavior>
|
||||
|
||||
## Diagnostics
|
||||
<--doctor output, launcher log, session env, anything else from the test's Diagnostics block>
|
||||
|
||||
## Notes
|
||||
<any hypotheses, related PRs, recent regressions>
|
||||
```
|
||||
|
||||
Link the issue back into [`matrix.md`](./matrix.md) on the affected cell using the standard format: `✗ #NNN`.
|
||||
|
||||
## Updating the matrix
|
||||
|
||||
One PR per sweep per row. Bundle every status change for that row into a single commit so the matrix history reads as a sequence of sweep events, not individual cell flips.
|
||||
|
||||
Commit message template:
|
||||
|
||||
```
|
||||
test(<row>): sweep <YYYY-MM-DD> — <project_version>+claude<upstream_version>
|
||||
|
||||
- T01 ? → ✓
|
||||
- T03 ? → ✓
|
||||
- T05 ? → ✗ (filed #NNN)
|
||||
- T17 ? → ✓
|
||||
- ...
|
||||
```
|
||||
|
||||
If the same sweep also turned up new tests worth adding, those go in a separate commit before the status update so the diff stays focused.
|
||||
|
||||
## Severity guidance for new tests
|
||||
|
||||
When adding a test to `cases/` or `ui/`, pick severity using these heuristics:
|
||||
|
||||
| Tier | Pick when | Example |
|
||||
|------|-----------|---------|
|
||||
| Smoke | First-launch experience; if this fails the app is unusable for normal users | T01 (app launch), T03 (tray), T16 (Code tab loads) |
|
||||
| Critical | Feature is documented in upstream docs **and** breaks core workflows when broken | T22 (PR monitoring), T34 (connector OAuth), T17 (folder picker) |
|
||||
| Should | Quality-of-life or documented edge case; users hit it but have a workaround | T28 (catch-up after suspend), S26 (auto-update vs apt) |
|
||||
| Could | Niche, env-specific, or graceful-degradation checks | T39 (`/desktop` CLI N/A), S22 (computer-use toggle absent on Linux) |
|
||||
|
||||
When in doubt, file as **Should**. Smoke and Critical mean release gates — be conservative about adding gates.
|
||||
|
||||
## Adding a new test
|
||||
|
||||
1. Pick the right surface file in `cases/` (or create one with prior buy-in if no existing surface fits — don't sprinkle new files lightly).
|
||||
2. Use the next free ID: highest `T##` + 1 for cross-env, highest `S##` + 1 for env-specific. Don't reuse retired IDs.
|
||||
3. Follow the standard structure: `**Severity:**`, `**Surface:**`, `**Applies to:**`, `**Steps:**`, `**Expected:**`, `**Diagnostics on failure:**`, `**References:**`.
|
||||
4. Add the row to [`matrix.md`](./matrix.md) with all-`?` initial state.
|
||||
5. Mention the new test in the PR description so reviewers know to read the spec.
|
||||
|
||||
For UI checklist additions, append rows to the relevant `ui/<surface>.md` table. UI rows don't need `T##` / `S##` IDs — the surface file + element name is the identity.
|
||||
|
||||
## Automated runs
|
||||
|
||||
The harness at [`tools/test-harness/`](../../tools/test-harness/) drives any
|
||||
test with a `runner:` field. As of 2026-04-30, that's T01, T03, T04, T17.
|
||||
|
||||
### Invoking a sweep
|
||||
|
||||
```sh
|
||||
cd tools/test-harness
|
||||
npm install # first time only
|
||||
ROW=KDE-W ./orchestrator/sweep.sh
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
- `results/results-${ROW}-${DATE}/junit.xml` — the JUnit summary (one
|
||||
testsuite per `.spec.ts` file, with the test's annotations preserved as
|
||||
metadata).
|
||||
- `results/results-${ROW}-${DATE}/test-output/<test>/` — per-test
|
||||
attachments (screenshots, launcher log, session env, frame extents,
|
||||
click-attempt diagnostics, etc.). Captured on every run, not just on
|
||||
failure (Decision 7).
|
||||
- `results/results-${ROW}-${DATE}/html/` — Playwright's HTML report.
|
||||
- `results/results-${ROW}-${DATE}.tar.zst` — bundled artifact for
|
||||
off-machine inspection (when `zstd` is available).
|
||||
|
||||
`sweep.sh` prints a summary line at the end:
|
||||
|
||||
```
|
||||
summary: tests=4 failures=0 errors=0 skipped=1
|
||||
```
|
||||
|
||||
### Translating results to the matrix
|
||||
|
||||
JUnit `<failure>` → `✗`, `<error>` (harness broke) → `?`, `<skipped>` →
|
||||
`-` (when intentionally not applicable) or stays `?` (when the test
|
||||
couldn't reach an assertion — common case for renderer tests that need
|
||||
sign-in or selectors that haven't been tuned). For now this mapping is
|
||||
manual: open `junit.xml`, update `matrix.md` cells, commit. A
|
||||
`render-matrix.sh` to do this automatically is on the to-do list.
|
||||
|
||||
### Coexistence with manual tests
|
||||
|
||||
Tests without a `runner:` continue to flow through the manual loop above.
|
||||
The matrix doesn't distinguish automated from manual cells — a `✓` is a
|
||||
`✓` regardless of how it was produced. The `runner:` field on each case
|
||||
makes the source-of-truth explicit per-test.
|
||||
|
||||
### Path through the CDP auth gate (why this works)
|
||||
|
||||
The shipped Electron exits if `--remote-debugging-port` is on argv
|
||||
without a valid `CLAUDE_CDP_AUTH` token. Both `_electron.launch()` and
|
||||
`chromium.connectOverCDP()` inject that flag. The harness sidesteps the
|
||||
gate by spawning Electron clean and attaching the Node inspector via
|
||||
`SIGUSR1` at runtime — same code path as `Developer → Enable Main
|
||||
Process Debugger`. From there, main-process JS evaluation reaches the
|
||||
renderer through `webContents.executeJavaScript()`. Full writeup:
|
||||
[`automation.md`](./automation.md#the-cdp-auth-gate-and-the-runtime-attach-workaround-that-beats-it).
|
||||
|
||||
### Wayland-mode sweep
|
||||
|
||||
Default backend is X11-via-XWayland (matches `launcher-common.sh`'s
|
||||
default). To sweep the suite under native Wayland, set
|
||||
`CLAUDE_HARNESS_USE_WAYLAND=1`:
|
||||
|
||||
```sh
|
||||
CLAUDE_HARNESS_USE_WAYLAND=1 ROW=KDE-W ./orchestrator/sweep.sh
|
||||
```
|
||||
|
||||
Every `launchClaude()` swaps to the Wayland flag set
|
||||
(`--ozone-platform=wayland` + WaylandWindowDecorations / IME / text-
|
||||
input-version=3, mirroring `scripts/launcher-common.sh:132-139`) and
|
||||
exports `CLAUDE_USE_WAYLAND=1` + `GDK_BACKEND=wayland` into the spawn
|
||||
env. Per-launch overrides via `launchClaude({ extraEnv })` still win,
|
||||
so a single test can opt back to X11 inside a Wayland-mode sweep.
|
||||
|
||||
Caveat: T04 (`_NET_FRAME_EXTENTS` xprop check) only works under
|
||||
XWayland — native-Wayland sessions have no X11 client list, so T04
|
||||
will skip with a "no X11 client list" diagnostic.
|
||||
|
||||
## Grounding sweep
|
||||
|
||||
Separate from the test sweep. Where the test sweep verifies *upstream
|
||||
Linux compat behavior* against case specs, the grounding sweep
|
||||
verifies *the specs themselves* against upstream behavior — making
|
||||
sure the Steps and Expected fields haven't bit-rotted past what the
|
||||
shipped build actually does. Run on every upstream `CLAUDE_DESKTOP_VERSION`
|
||||
bump.
|
||||
|
||||
### Static pass
|
||||
|
||||
For each file under [`cases/`](./cases/), confirm every test's
|
||||
`**Code anchors:**` field still resolves and the Steps/Expected match
|
||||
behavior. The convention is documented in
|
||||
[`cases/README.md`](./cases/README.md#anchor-scope) — anchors are
|
||||
either upstream code (`build-reference/app-extracted/.vite/build/`),
|
||||
wrapper scripts (`scripts/`), v7 walker inventory, or out-of-scope
|
||||
(CLI binary, server-rendered SPA).
|
||||
|
||||
When a test drifts, edit Steps/Expected in place. When a feature is
|
||||
gone from the build, prepend
|
||||
`> **⚠ Missing in build X.Y.Z** — <note>. Re-verify after next
|
||||
upstream bump.` under the test heading.
|
||||
|
||||
### Runtime pass
|
||||
|
||||
Run [`tools/test-harness/grounding-probe.ts`](../../tools/test-harness/grounding-probe.ts)
|
||||
against the live build:
|
||||
|
||||
```sh
|
||||
cd tools/test-harness
|
||||
npm run grounding-probe -- --launch --include-synthetic \
|
||||
--out ../../docs/testing/cases-grounding-runtime.json
|
||||
```
|
||||
|
||||
Captures runtime state for tests where static greps can't disambiguate
|
||||
(IPC handler registry, `globalShortcut.isRegistered()` for known
|
||||
accelerators, `app.getLoginItemSettings()`, `safeStorage`,
|
||||
`autoUpdater.getFeedURL()`, SNI tray registration, AX-tree fingerprint
|
||||
of whatever's on screen). Output is keyed by test ID — diff against
|
||||
the previous version's capture to spot drift the static pass missed.
|
||||
|
||||
Surfaces inside modals or popups (T22 PR toolbar, T26 preset list,
|
||||
T31 side chat, T32 slash menu) need the surface open at probe time.
|
||||
Open the relevant view in the running app before re-running with
|
||||
`--port 9229` (attach mode).
|
||||
Reference in New Issue
Block a user