Compare commits

...

7 Commits

Author SHA1 Message Date
aaddrick
5cebb5c213 test: consolidate log_session_env BATS coverage (#548)
Collapse the four log_session_env cases into two, and tighten the
assertions in both:

  - Old test 1 (substring match per key) + old test 4 (block braces
    on their own lines) → one test using exact-line equality on the
    `lines` array. Locks block structure and per-key formatting in
    a single pass; substring matching could not catch a regression
    that re-ordered keys, dropped indentation, or merged lines.
  - Old test 2 (unset values are KEY=) + old test 3 (empty-string
    is KEY=) → one test covering both code paths. Exact-line match
    proves the value after `=` is truly empty; the previous
    `*'KEY='*` substring would have matched `KEY=value` and the
    old test-3 regex was fragile (depended on trailing newline
    being literal `$'\n'` vs end-of-string `$`).

Net: 77 → 42 lines, 4 → 2 cases, stronger guarantees. No change to
the helper itself or the call sites — issue #548 acceptance criteria
still hold.
2026-05-03 17:17:26 -04:00
aaddrick
77d9b80a19 launcher: log session/IME env block at startup (#548)
Adds log_session_env, called once per launch from each packaging
target (deb, rpm, AppImage, Nix). Emits a single env={ ... } block
covering display (XDG_SESSION_TYPE, WAYLAND_DISPLAY, DISPLAY,
XDG_CURRENT_DESKTOP), IME (GTK_IM_MODULE, XMODIFIERS, QT_IM_MODULE),
and Claude-specific overrides (CLAUDE_USE_WAYLAND,
CLAUDE_TITLEBAR_STYLE, CLAUDE_GTK_IM_MODULE).

Empty/unset values are emitted as `KEY=` (rather than omitted) so
absence is unambiguous in bug reports. Pure observability — no
behavior change.

Closes #548

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-03 17:14:51 -04:00
Aaddrick
b351d42a2d docs: archive upstream report draft for #546 (filed as anthropics/claude-code#55353) (#552)
* docs(upstream-reports): draft anthropics/claude-code report for #546

Adds a ready-to-file draft of the upstream MCP double-spawn report,
matched to the anthropics/claude-code bug_report.yml schema and
written in aaddrick's documented voice.

Includes a filing checklist (GitHub issue + in-app /bug + bidirectional
comment back on #546) and a note about the template mismatch since the
form is built for the Claude Code CLI rather than Claude Desktop.

Refs #546

Co-Authored-By: Claude <claude@anthropic.com>

* docs(upstream-reports): link claude-desktop-debian repo in draft body

Co-Authored-By: Claude <claude@anthropic.com>

* docs(upstream-reports): add download-rate context next to repo link

Adds an approximate "~2,300 package downloads/day across the last 3
releases" parenthetical so the upstream report leads with a sense of
how many users the bug affects.

Computed from GitHub release asset download counts: 13,823 installable
binary downloads (deb + rpm + AppImage, both arches) across 1.5354.0,
1.5220.0, and 1.4758.0 over a ~6-day window.

Co-Authored-By: Claude <claude@anthropic.com>

* docs(upstream-reports): voice pass on draft body and title

Refines the draft for the upstream `anthropics/claude-code` issue
through the aaddrick-voice profile. Changes are surgical:

- Title: em-dash separator → colon (matches voice's documented
  preference for colons; removes em-dash signal site-wide).
- "What's Wrong?": opens with personal-experience framing ("I was
  reading", "What I found"), splits compound sentences, swaps
  announcement-colons for periods.
- "What Should Happen?": same period-for-colon swap on the fix
  paragraph.

No content shifts. Symbol table, code blocks, log signals, links,
and form-field labels untouched.

Co-Authored-By: Claude <claude@anthropic.com>

* docs(upstream-reports): drop bogus /bug filing step

User confirmed `/bug` and `/feedback` are inert in both Claude
Desktop and Claude Code. Earlier web research suggesting they route
to engineering was wrong. Replaced step 4 of the filing checklist
with a note about what's actually in the Help menu (Get Support
goes to the support chat, wrong queue) and what the Troubleshooting
submenu IS useful for (attaching Installation ID / logs to a
GitHub issue).

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-03 12:38:12 -04:00
Aaddrick
b404ebd5f1 docs(learnings): refine mcp-double-spawn root cause and routing (#546) (#547)
Per-coordinator-registry framing (CCD + LAM + SshMcpServerManager)
replaces the previous two-coordinator framing. Notes that each
coordinator dedups within its own scope, so the bug is strictly
cross-coordinator. Routing correction: the SDK does what it's told
- the bug is in Claude Desktop's coordinator wiring, so the SDK
repo is only a defensible secondary venue for advocacy of a
shared-transport/multiplex primitive. Symbol drift section points
at #546 for current minified symbols and extraction regexes.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-03 12:37:48 -04:00
Aaddrick
14d04c2dab fix(dnf): set metadata_expire=1h on generated .repo (#551)
DNF defaults to a 48h metadata cache when metadata_expire is unset,
so users running `dnf install/reinstall claude-desktop` shortly after
a release see stale versions until either the cache expires or they
manually run `dnf clean expire-cache`.

Lower the cache TTL on the generated repo file so freshly published
releases propagate within an hour without user intervention.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-03 12:37:06 -04:00
aaddrick
fd352f4390 docs(readme): credit jslatten for KDE Wayland desktopName fix (#562)
Co-Authored-By: Claude <claude@anthropic.com>
2026-05-03 08:22:32 -04:00
Justin Slatten
5a98854137 set desktopName for Wayland grouping (#562) 2026-05-03 08:20:09 -04:00
13 changed files with 331 additions and 31 deletions

View File

@@ -674,6 +674,7 @@ jobs:
'gpgcheck=1' \
'repo_gpgcheck=1' \
'gpgkey=https://pkg.claude-desktop-debian.dev/KEY.gpg' \
'metadata_expire=1h' \
> rpm/claude-desktop.repo
- name: Re-upload signed RPMs to GitHub Release

View File

@@ -259,6 +259,7 @@ Special thanks to:
- **[zabka](https://github.com/zabka)** for identifying that `cowork-vm-service.js` was never auto-spawned on Linux and contributing a systemd-unit workaround that scoped the daemon auto-launch fix (#445)
- **[sirfaber](https://github.com/sirfaber)** for fixing the `$`-in-minified-identifier breakage of cowork Patch 2b (vm module assignment) and Patch 6 step 2 (retry-delay auto-launch) on Claude Desktop 1.5354.0 (#555)
- **[ProfFlow](https://github.com/ProfFlow)** for re-fixing the RPM repodata signing regression by appending `!` to the keyid passed to `gpg --default-key`, forcing `repomd.xml` to be signed by the primary key instead of the auto-selected signing subkey (#566)
- **[jslatten](https://github.com/jslatten)** for fixing the KDE Plasma Wayland launcher-grouping bug by setting `pkg.desktopName` in the packaged `app.asar`'s `package.json`, format-conditional so deb/rpm get `claude-desktop.desktop` and AppImage gets `io.github.aaddrick.claude-desktop-debian.desktop` (#562)
## Sponsorship

View File

@@ -37,46 +37,67 @@ external services with single-connection contracts, etc.
## Root Cause (Upstream)
Two parallel session managers live inside Electron main, each
holding an independent Claude Agent SDK `query`:
Multiple session managers live inside Electron main, each
holding its own MCP coordinator state with its own registry. The
two that spawn stdio MCPs from `claude_desktop_config.json` and
trigger this bug:
| Manager class | IPC namespace | Coordinator | Logs prefix |
|--------------------------|------------------------------------------|-----------------|-------------|
| `LocalSessions` | `claude.web_$_LocalSessions_$_*` | `n2t("ccd")` | `[CCD]` |
| `LocalAgentModeSessions` | `claude.web_$_LocalAgentModeSessions_$_*`| `n2t("cowork")` | `[LAM]` |
A third coordinator class — `SshMcpServerManager` — follows the
same per-coordinator-registry pattern but uses an SSH transport
and doesn't contribute to the local-node double-spawn. Its
existence does say something about the design intent: per-
coordinator isolated state appears to be a deliberate
architectural pattern, not a one-off oversight.
The logs prefixes are what to grep `~/.config/Claude/logs/` for to
confirm a session is hitting both coordinators (and therefore this
bug specifically).
Each `query` holds its own SDK transport. The transport's
`spawnLocalProcess` (`Du.spawn`) launches stdio MCPs **without
consulting the global registry** that *would* dedupe them
(`hZ` map, accessed via `oUt(serverName)` /
`launchMcpServer`). That registry is only used for the
"internal" cowork in-process MessageChannelMain path.
Each coordinator dedups **within its own scope**: CCD's launch
function serializes per server name through a promise queue and
shuts down any prior entry before respawn; LAM's
`getOrCreateConnection` reuses connected entries from its own
`connections` Map. The double-spawn is strictly **cross-
coordinator** — one process per coordinator that has the server
in its config.
In current versions (verified against `1.5354.0`) both
coordinators route their transport creation through a shared
Claude Desktop-side factory, but the factory itself doesn't
dedupe and the per-coordinator registries above it aren't
unified.
Net result: 2 coordinators × N configured MCPs = 2N processes.
Symbol names (`n2t`, `hZ`, `oUt`, `LocalSessions`,
`LocalAgentModeSessions`) are minified and **will rename across
upstream releases**.
### Symbol drift
Minified symbols rename across upstream releases. Issue
[#546](https://github.com/aaddrick/claude-desktop-debian/issues/546)
maintains the current symbol mappings (verified against
`1.5354.0`) plus extraction regexes that work against both
minified and beautified bundles.
## Status
**Upstream Claude Desktop bug. Not patchable in this repo.** A
fix would require either:
**Upstream Claude Desktop bug. Not patchable in this repo.** The
proximate cause is in Claude Desktop's session manager wiring. A
real fix needs either:
- Routing the SDK stdio transport through `oUt`/`hZ` (the
existing serialized-per-name registry), or
- Sharing one MCP-server registry between the `ccd` and
`cowork` coordinators.
- LAM proxying its MCP traffic through CCD's existing connection
(so only one coordinator owns the spawn), or
- A multiplexing wrapper transport that lets one spawned stdio
child serve multiple SDK clients via demuxing.
Both live inside the closed-source SDK transport / session
manager wiring. Regex-matching the minified symbols from
`scripts/patches/` would be fragile against release-to-release
renames and exceeds this repo's "minimal Linux-compat patches
only" charter.
Stdio MCP is 1:1 at the protocol layer — one stdin/stdout pair,
one transport, one SDK client. Sharing one process across
coordinators requires real engineering, not a sed patch on
minified code, and exceeds this repo's "minimal Linux-compat
patches only" charter.
## What's Already Verified Clean
@@ -118,13 +139,15 @@ The reporter's `baro-voyager` MCP shipped both in commit
- **Primary:** in-app feedback (Help → Send Feedback) or
`support@anthropic.com`. The duplication happens in
closed-source Desktop main.
- **Secondary:** an SDK-transport-flavored issue on
closed-source Desktop main, in the per-coordinator registry
wiring.
- **Secondary:** an issue on
[`anthropics/claude-agent-sdk-typescript`](https://github.com/anthropics/claude-agent-sdk-typescript)
is defensible — the spawn path goes through the **Claude Agent
SDK's** `query` transport (`spawnLocalProcess` / `Du.spawn`),
which is shared surface area. Reference the missing `hZ`
consultation explicitly.
is defensible only if it advocates for a shared-transport /
multiplex primitive that would make this kind of bug
structurally harder. The SDK's spawn implementation is doing
what it's told — the bug is one layer up, in Claude Desktop
calling spawn from two separate coordinators.
The embedded Claude Code CLI subprocess inside Claude Desktop is
**not** the cause — it receives `--mcp-config` only when the

View File

@@ -0,0 +1,164 @@
# Upstream report draft: MCP double-spawn (issue #546)
This is the draft for the upstream bug report covering [#546](https://github.com/aaddrick/claude-desktop-debian/issues/546). Filing target is `anthropics/claude-code` GitHub Issues, with an in-app `/bug` from Claude Desktop as a complement so the report ties to build telemetry.
## Template mismatch note
The `anthropics/claude-code` bug template is built for the Claude Code CLI, not Claude Desktop. Required fields like "Claude Code Version" and "Terminal/Shell" don't apply cleanly. Other Claude Desktop bug reports in the same repo work around this by putting `N/A — Claude Desktop <version>` in the version field and selecting `Other` for terminal (see #43705, #36319, #14807).
## Title
```
[BUG] Claude Desktop 1.5354.0: stdio MCP servers double-spawn from independent CCD/LAM coordinator registries
```
## Form fields
### Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
### What's Wrong?
I maintain [claude-desktop-debian](https://github.com/aaddrick/claude-desktop-debian) (~2,300 package downloads/day across the last 3 releases), which repackages the Windows Electron build for Linux. I was reading the MCP spawn path in 1.5354.0 and found that stdio MCP servers configured in `claude_desktop_config.json` get spawned twice when both the chat panel and Code/Agent panel are active.
The user-visible symptom is two `node` processes per MCP, both children of the Electron main PID. Killing one disconnects one panel and the other keeps working. They're independent client/server pairs with no failover between them.
The original symptom report came from @communitytranslations against an earlier build (tracked in our repo as #526). I went back and read the bundle to confirm the cause. What I found was different from what we'd previously documented.
CCD wraps the spawn path in a per-key promise queue keyed by server name. It shuts down any prior entry in its global registry Map before respawning. That's correct dedup within CCD. But LAM (`LocalMcpServerManager`) has its own `this.connections` Map and its own `getOrCreateConnection` path. It never consults CCD's registry.
CCD and LAM each maintain independent spawn lifecycle management. They each spawn their own copy of the same MCP server. The double-spawn is structural in the current architecture. Each coordinator legitimately holds its own connection.
There's also a third coordinator class, `SshMcpServerManager`, that follows the same per-coordinator-registry pattern. It uses an SSH transport, so it doesn't contribute to local-node double-spawn directly. Its existence suggests per-coordinator isolated state is a deliberate pattern, not a one-off.
Secondary bug worth flagging while you're in this code. The `child_process.spawn` wrapper does proper signal escalation (end stdin, wait 2s, SIGTERM, wait 2s, SIGKILL). The `utilityProcess.fork` wrapper doesn't. It sends `process.kill()` (default SIGTERM), waits 5s, then calls `kill()` again with the same default signal. No SIGKILL escalation. A built-in-node MCP server that ignores SIGTERM could leak as an orphaned utility process.
### What Should Happen?
One process per stdio MCP server entry in `claude_desktop_config.json`, regardless of how many panels are open. Resource-side that means no more 2x memory and 2x stdin/stdout traffic per server. User-side that means `ps` shows one entry per declared server.
The fix is architectural. CCD and LAM share a registry, or the local-spawn factory dedups at the transport layer, or LAM proxies through CCD when running in-process. Any of those would collapse the duplication.
### Error Messages/Logs
The user-facing log prefixes are stable across releases. Grep `~/.config/Claude/logs/` for:
```
[CCD]
[LAM]
[LocalMcpServerManager]
[SshMcpServerManager]
```
For the spawn lifecycle specifically, look for:
```
"Launching MCP Server: <name>" (CCD spawn entry)
"Shutting down MCP Server: <name>" (CCD shutdown entry)
"local-mcp-server-cleanup" (LAM cleanup path)
```
Two of these per declared MCP server is the diagnostic signal.
### Steps to Reproduce
1. Linux host running Claude Desktop at or near 1.5354.0
2. Declare at least one stdio MCP server in `~/.config/Claude/claude_desktop_config.json`
3. Open Claude Desktop, start a session, open the Code/Agent panel and let it initialize fully (the original report waited about 5 minutes)
4. `ps -ef | grep <server-binary-name>`
Expected: 1 process per MCP. Actual: 2 processes per MCP, both children of the same Electron main PID.
### Claude Model
Not sure / Multiple models
### Is this a regression?
I don't know
### Last Working Version
(leave blank)
### Claude Code Version
```
N/A — this is a Claude Desktop issue. Bundle version: 1.5354.0
```
### Platform
Anthropic API
### Operating System
Ubuntu/Debian Linux
### Terminal/Shell
Other
### Additional Information
Bundle reference table for 1.5354.0. Symbols rename across releases, so each row has a stable string anchor for re-finding them.
| Role | Symbol in 1.5354.0 | Stable anchor |
|---|---|---|
| CCD spawn function | `BPt` | `"Launching MCP Server:"` |
| CCD shutdown function | `CPt` | `"Shutting down MCP Server:"` |
| CCD per-key promise queue | `dPt` | called by CCD spawn fn: `await dPt(e, async () => {...})` |
| CCD server registry Map | `xX` | `.get()` immediately preceding the CCD shutdown log line |
| Shared transport factory | `oPt` | `"built-in-node"` literal in factory body |
| LAM manager class | `p0A` | `"[LocalMcpServerManager]"` or `"local-mcp-server-cleanup"` |
| SSH manager class | `Rde` | `"[SshMcpServerManager]"` or `"ssh-mcp-server-cleanup"` |
| `utilityProcess.fork` wrapper | `mFr` | constructed in shared factory's `built-in-node` branch |
| `child_process.spawn` wrapper | `tFr` | constructed in shared factory's default branch |
Extraction commands (verified against 1.5354.0):
```bash
cd build-reference/app-extracted/.vite/build
# CCD spawn function name
grep -Pzo 'async function \K\w+(?=\(\w*\)\s*\{(?s).{0,800}?Launching MCP Server)' index.js | tr '\0' '\n'
# Shared transport factory (anchored on the unique 'built-in-node' string)
grep -Pzo 'async function \K\w+(?=\([^)]*\)\s*\{(?s).{0,400}?built-in-node)' index.js | tr '\0' '\n'
# All coordinator classes following the per-coordinator-registry pattern
grep -Pzo 'class \K\w+(?=\s*\{(?s).{0,300}?this\.connections\s*=\s*new Map)' index.js | tr '\0' '\n'
# LAM manager class specifically
grep -Pzo 'class \K\w+(?=\s*\{(?s).{0,500}?local-mcp-server-cleanup)' index.js | tr '\0' '\n'
```
Two questions where a one-line answer from the team would help us route this downstream:
1. Is per-coordinator isolated state intentional, or is it legacy drift from when each coordinator instantiated its transport inline?
2. Is the recent extraction of the shared transport factory (`oPt`) the start of a dedup refactor, or incidental cleanup?
If (1) is "intentional," we'll point users at the lockfile workaround as the supported path. If (2) is "in progress," this report saves you the duplicate analysis work.
Full provenance: [aaddrick/claude-desktop-debian#546](https://github.com/aaddrick/claude-desktop-debian/issues/546). Related learnings doc updates: [#527](https://github.com/aaddrick/claude-desktop-debian/pull/527) and [#547](https://github.com/aaddrick/claude-desktop-debian/pull/547).
## Filing checklist
When you're ready to file:
1. Open https://github.com/anthropics/claude-code/issues/new?template=bug_report.yml
2. Paste each section above into the matching form field
3. Submit
4. Drop the GitHub issue URL as a comment on [#546](https://github.com/aaddrick/claude-desktop-debian/issues/546) so the trail is bidirectional
Note: there is no in-app engineering bug-report path in Claude Desktop. `/bug` and `/feedback` are inert. The Help menu has "Get Support" (routes to the support chat, wrong queue for engineering) and "Troubleshooting" (self-diagnostic — useful for attaching `Copy Installation ID` or `Show Logs in File Manager` output to a GitHub issue, but not a reporting step on its own).
## Voice and authorship
Drafted using the [aaddrick-voice](https://github.com/aaddrick/written-voice-replication/blob/78f178dcf832943bcf1d5a65bf7627c3a20053a6/.claude/agents/aaddrick-voice.md) style profile against the form schema in `anthropics/claude-code/.github/ISSUE_TEMPLATE/bug_report.yml`.
---
Written by Claude Opus 4.7 via [Claude Code](https://claude.ai/code)

View File

@@ -245,6 +245,7 @@ cleanup_stale_cowork_socket
log_message '--- Claude Desktop Launcher Start (NixOS) ---'
log_message "Timestamp: $(date)"
log_message "Arguments: $@"
log_session_env
# Check for display
if ! check_display; then

View File

@@ -16,6 +16,39 @@ log_message() {
echo "$1" >> "$log_file"
}
# Log the session/IME environment vars that drive display and input
# decisions, so bug reports include enough context to reason about
# them without round-trip env-dump requests (#548).
#
# Emits one block:
# env={
# KEY=value
# ...
# }
#
# Empty or unset values are emitted as `KEY=` so absence is
# unambiguous (vs. silently omitted). Caller must run setup_logging
# first.
log_session_env() {
local key
log_message 'env={'
for key in \
XDG_SESSION_TYPE \
WAYLAND_DISPLAY \
DISPLAY \
XDG_CURRENT_DESKTOP \
GTK_IM_MODULE \
XMODIFIERS \
QT_IM_MODULE \
CLAUDE_USE_WAYLAND \
CLAUDE_TITLEBAR_STYLE \
CLAUDE_GTK_IM_MODULE
do
log_message " $key=${!key:-}"
done
log_message '}'
}
# Detect display backend (Wayland vs X11)
# Sets: is_wayland, use_x11_on_wayland
detect_display_backend() {

View File

@@ -98,6 +98,7 @@ log_message '--- Claude Desktop AppImage Start ---'
log_message "Timestamp: $(date)"
log_message "Arguments: $@"
log_message "APPDIR: $appdir"
log_session_env
# Path to the bundled Electron executable and app
electron_exec="$appdir/usr/lib/node_modules/electron/dist/electron"

View File

@@ -114,6 +114,7 @@ cleanup_stale_cowork_socket
log_message '--- Claude Desktop Launcher Start ---'
log_message "Timestamp: \$(date)"
log_message "Arguments: \$@"
log_session_env
# Check for display
if ! check_display; then

View File

@@ -97,6 +97,7 @@ cleanup_stale_cowork_socket
log_message '--- Claude Desktop Launcher Start ---'
log_message "Timestamp: \$(date)"
log_message "Arguments: \$@"
log_session_env
# Check for display
if ! check_display; then

View File

@@ -37,16 +37,21 @@ EOFENTRY
# Update package.json
echo 'Modifying package.json to load frame fix and add node-pty...'
local desktop_name='claude-desktop.desktop'
if [[ ${build_format:-} == 'appimage' ]]; then
desktop_name='io.github.aaddrick.claude-desktop-debian.desktop'
fi
node -e "
const fs = require('fs');
const pkg = require('./app.asar.contents/package.json');
pkg.originalMain = pkg.main;
pkg.main = 'frame-fix-entry.js';
pkg.desktopName = process.argv[1];
pkg.optionalDependencies = pkg.optionalDependencies || {};
pkg.optionalDependencies['node-pty'] = '^1.0.0';
fs.writeFileSync('./app.asar.contents/package.json', JSON.stringify(pkg, null, 2));
console.log('Updated package.json: main entry and node-pty dependency');
"
console.log('Updated package.json: main entry, desktopName, and node-pty dependency');
" "$desktop_name"
# Create stub native module
echo 'Creating stub native module...'

View File

@@ -35,10 +35,15 @@ setup() {
unset CLAUDE_USE_WAYLAND
unset NIRI_SOCKET
unset XDG_CURRENT_DESKTOP
unset XDG_SESSION_TYPE
unset CLAUDE_MENU_BAR
unset CLAUDE_TITLEBAR_STYLE
unset COWORK_VM_BACKEND
unset ELECTRON_USE_SYSTEM_TITLE_BAR
unset GTK_IM_MODULE
unset XMODIFIERS
unset QT_IM_MODULE
unset CLAUDE_GTK_IM_MODULE
# shellcheck source=scripts/launcher-common.sh
source "$SCRIPT_DIR/../scripts/launcher-common.sh"
@@ -86,6 +91,63 @@ teardown() {
[[ "${lines[1]}" == "test message two" ]]
}
# =============================================================================
# log_session_env
# =============================================================================
@test "log_session_env: emits env={ ... } block with all required keys" {
setup_logging
XDG_SESSION_TYPE='wayland'
WAYLAND_DISPLAY='wayland-0'
DISPLAY=':0'
XDG_CURRENT_DESKTOP='KDE'
GTK_IM_MODULE='ibus'
XMODIFIERS='@im=ibus'
QT_IM_MODULE='ibus'
CLAUDE_USE_WAYLAND='1'
CLAUDE_TITLEBAR_STYLE='hybrid'
CLAUDE_GTK_IM_MODULE='xim'
log_session_env
run cat "$log_file"
# Exact-line match locks block structure (open/close braces on
# their own lines) and per-key formatting in one pass.
[[ "${lines[0]}" == 'env={' ]]
[[ "${lines[1]}" == ' XDG_SESSION_TYPE=wayland' ]]
[[ "${lines[2]}" == ' WAYLAND_DISPLAY=wayland-0' ]]
[[ "${lines[3]}" == ' DISPLAY=:0' ]]
[[ "${lines[4]}" == ' XDG_CURRENT_DESKTOP=KDE' ]]
[[ "${lines[5]}" == ' GTK_IM_MODULE=ibus' ]]
[[ "${lines[6]}" == ' XMODIFIERS=@im=ibus' ]]
[[ "${lines[7]}" == ' QT_IM_MODULE=ibus' ]]
[[ "${lines[8]}" == ' CLAUDE_USE_WAYLAND=1' ]]
[[ "${lines[9]}" == ' CLAUDE_TITLEBAR_STYLE=hybrid' ]]
[[ "${lines[10]}" == ' CLAUDE_GTK_IM_MODULE=xim' ]]
[[ "${lines[11]}" == '}' ]]
}
@test "log_session_env: unset/empty values render as 'KEY=' (no value)" {
setup_logging
# All vars unset by setup() except this one, which exercises the
# empty-string branch (must be indistinguishable from unset).
GTK_IM_MODULE=''
log_session_env
run cat "$log_file"
# Exact-line match proves the line ends right after '=' — a
# substring like *'KEY='* would also match 'KEY=value'.
[[ "${lines[1]}" == ' XDG_SESSION_TYPE=' ]]
[[ "${lines[2]}" == ' WAYLAND_DISPLAY=' ]]
[[ "${lines[3]}" == ' DISPLAY=' ]]
[[ "${lines[4]}" == ' XDG_CURRENT_DESKTOP=' ]]
[[ "${lines[5]}" == ' GTK_IM_MODULE=' ]]
[[ "${lines[6]}" == ' XMODIFIERS=' ]]
[[ "${lines[7]}" == ' QT_IM_MODULE=' ]]
[[ "${lines[8]}" == ' CLAUDE_USE_WAYLAND=' ]]
[[ "${lines[9]}" == ' CLAUDE_TITLEBAR_STYLE=' ]]
[[ "${lines[10]}" == ' CLAUDE_GTK_IM_MODULE=' ]]
}
# =============================================================================
# check_display
# =============================================================================

View File

@@ -94,7 +94,7 @@ assert_contains "$appdir/AppRun" 'build_electron_args' \
# --- App contents (asar) ---
resources_dir="$appdir/usr/lib/node_modules/electron/dist/resources"
validate_app_contents "$resources_dir"
validate_app_contents "$resources_dir" "${component_id}.desktop"
# --- Cleanup ---
rm -rf "$extract_dir"

View File

@@ -59,8 +59,10 @@ assert_command_succeeds() {
# Validate app contents inside an Electron resources directory.
# $1 = path to the resources/ dir containing app.asar
# $2 = expected desktopName in app/package.json
validate_app_contents() {
local resources_dir="$1"
local expected_desktop_name="${2:-claude-desktop.desktop}"
assert_file_exists "$resources_dir/app.asar"
assert_dir_exists "$resources_dir/app.asar.unpacked"
@@ -95,6 +97,11 @@ validate_app_contents() {
'frame-fix-entry.js' \
"package.json main field references frame-fix-entry.js"
# package.json desktopName matches the installed desktop file
assert_contains "$extract_dir/app/package.json" \
"\"desktopName\": \"$expected_desktop_name\"" \
"package.json desktopName matches $expected_desktop_name"
# .vite/build/index.js exists (main process code)
assert_file_exists "$extract_dir/app/.vite/build/index.js"