# GUI → PKG/QA requests (M1) Filed by lane GUI. These touch PKG/QA-owned files (`tools/bootstrap.sh`, the CI jobs, `docs/agents/AGENT-PKG-QA.md`) and the root `README.md`, so GUI is not making the edits — CLAUDE.md §1. Everything below is apply-ready. --- ## R1 — `libqt6svg6-dev` is not a real package on 26.04 (hard configure failure now) `apt-cache policy libqt6svg6-dev` on a clean Ubuntu 26.04 box gives `Candidate: (none)`. The Qt 6 SVG **dev** package in the 26.04 archive is **`qt6-svg-dev`** (currently `6.10.2-2`). `libqt6svg6` (no `-dev`) exists as the runtime lib but carries no headers or CMake config, so `find_package(Qt6 COMPONENTS Svg)` fails without `qt6-svg-dev`. I checked the other 38 apt names in `bootstrap.sh` against `apt-cache policy` on 26.04 — `libqt6svg6-dev` is the only one with no candidate. Everything else resolves. **This is no longer a latent bug.** `gui/CMakeLists.txt` is now on `main`, so the root `CMakeLists.txt` line ```cmake find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools) ``` is live for every build. On a machine without the SVG dev package that is a hard configure failure for the whole project — not a skipped guard, not a GUI-only problem. **Why nothing has gone red yet — and why that's R2's problem too:** * This dev box already has `qt6-svg-dev` (pulled in by a `qt6-base-dev` recommends chain), so `pkg-config --exists Qt6Svg` succeeds and `--check` is green here. * CI runs on `ubuntu-latest`, which is **24.04**, where `libqt6svg6-dev` most likely still resolves. The project *targets* **26.04** (the `ci.yml` env comment says as much). So the one automated place that runs `bootstrap.sh` is on the wrong release to see this, and `--check` wouldn't catch it there anyway (R2). Wrong package name + runner-vs-target release mismatch means the class of bug PKG/QA owns is currently unobservable in CI. ### Fix — three files, same one-line change **`tools/bootstrap.sh`** (in `APT_GUI`, ~line 87): ```diff APT_GUI=( qt6-base-dev qt6-tools-dev qt6-tools-dev-tools # lrelease/lupdate for i18n - libqt6svg6-dev # GUI: SVG icon rendering + qt6-svg-dev # GUI: SVG icon rendering (was libqt6svg6-dev — no such + # package on 26.04; this is the one with headers + + # the Qt6SvgConfig.cmake find_package needs) ) ``` **`docs/agents/AGENT-PKG-QA.md`** (~line 17): ```diff - `libqt6svg6-dev`, `libsecret-1-dev`, `nodejs`/`npm` and (optionally) `clang` are + `qt6-svg-dev`, `libsecret-1-dev`, `nodejs`/`npm` and (optionally) `clang` are ``` **`README.md`** (toolchain table, ~line 106): ```diff sudo apt update && sudo apt install -y \ - libqt6svg6-dev \ # GUI: SVG icon rendering + qt6-svg-dev \ # GUI: SVG icon rendering libsecret-1-dev \ # DAEMON: Secret Service for site logins ``` --- ## R2 — `--check` verifies the outcome, not the thing that can break `--check` today runs the "verify toolchain" block: `command -v` for the binaries, `pkg-config --exists` for the dev libs (`Qt6Core`, `Qt6Widgets`, `Qt6Svg`, `libcurl`, `sqlite3`, `libssl`, `libsecret-1`, `libavformat`), plus the cmake/g++/node version floors. Every one of those checks a *result* on a box that already installed everything. None of them touch the apt package **names** in `APT_*`, which is the only part of the script that can be wrong on a clean machine — as R1 just demonstrated. So `--check` reported green for a script that fails `apt-get install` on its target OS. ### Proposed fix — make `--check` validate the install list it would actually run Add this to the verify block (runs in **both** modes; in `--check` mode it is the point of the exercise). It needs neither root nor network — `apt-cache policy` reads the local package lists that `apt-get update` already populated: ```bash # Every apt name this script would install must be a real, installable package on THIS # release. This is the check that would have caught libqt6svg6-dev before the VM did. log "validating apt package names (${#PKGS[@]})" missing_pkgs=() for pkg in "${PKGS[@]}"; do # `apt-cache policy ` prints "Candidate: (none)" for a name with no installable # version, and exits 0 either way — so grep the candidate line, don't trust $?. cand="$(apt-cache policy "$pkg" 2>/dev/null | sed -n 's/^ Candidate: //p')" if [[ -z "$cand" || "$cand" == "(none)" ]]; then printf ' \033[31m✗\033[0m %-20s no installable candidate on this release\n' "$pkg" missing_pkgs+=("$pkg") fail=1 fi done if (( ${#missing_pkgs[@]} == 0 )); then printf ' \033[32m✓\033[0m %-20s all %d resolve\n' "apt names" "${#PKGS[@]}" fi ``` Notes for whoever applies it: * `PKGS` is already assembled above the `CHECK_ONLY` branch, and it already respects `--with-clang` / `--with-packaging`, so the loop covers exactly what would be installed. * Belt-and-braces alternative: `apt-get install -s --no-install-recommends "${PKGS[@]}"` (`-s` = simulate, no root) also catches an *un-satisfiable dependency*, not just a missing name. Downside: it needs the apt lists reasonably fresh and is noisier to parse. `apt-cache policy` is enough to catch the whole class of bug in R1; pick whichever you want to own. * **The runner mismatch is part of this.** Even with the name-validation loop, the `bootstrap-script` job on a 24.04 runner validates names against the *24.04* archive, not 26.04. So R2 only fully closes the gap if the check also runs where the project ships — a 26.04 container step in that job (`container: ubuntu:26.04`), or a documented decision that 24.04 is close enough and why. Without that, a name that is valid on 24.04 and gone on 26.04 (exactly `libqt6svg6-dev`) still slips through. --- ## R3 — the GUI DoD gates have nowhere to run in CI — RESOLVED, ready to wire in **Status: done on GUI's side.** The harness `tests/integration/README.md` was waiting on now exists, builds, and has been run end-to-end (all three gates, all three unhappy-path sub-phases) against a real `mockd` with no changes needed to the pre-drafted job below. **Path:** `gui/tests/dod/run.sh [--json ]`, exactly the contract `tests/integration/README.md` specifies. `` is `scroll-60fps` | `rss-flat` | `unhappy-path`. It builds and tears down its own `mockd` (isolated `XDG_RUNTIME_DIR` via `mktemp -d`), needs no network, and leaves nothing running on any exit path (`trap cleanup EXIT INT TERM`) — verified live by checking for orphaned `tsx`/`mockd` processes after both a passing and a forced-failing run of each gate. **One change from the pre-drafted job:** the "Configure + build" step needs to also build the harness binary, not just `velox-gui`: ```diff - cmake --build --preset dev --target velox-gui + cmake --build --preset dev --target velox-gui gui-dod-harness ``` Nothing else in the pre-drafted YAML needs to change — the `# TODO(GUI): path` comment on the `scroll-60fps` line can just come off along with the marker on the line below it. **`rss-flat`'s slack: 20 MiB (`VELOX_DOD_RSS_SLACK_KIB`, default `20480`).** Chosen by running the gate locally (`VELOX_DOD_RSS_DURATION_SEC=8` override, i.e. not the real 10-minute number) a handful of times against `mockd --tasks 10000 --seed 1` and looking at actual post-warm-up growth (single-digit MiB per run here) — 20 MiB gives real headroom above that noise floor without being so loose a genuine per-tick leak in the progress-patch path could hide under it. This has **not** been proven against a full real 10-minute sanitized run (that's the nightly job's own first execution) or tuned against production-length data yet; treat it as GUI's stated starting number, not a load-tested constant, and expect it may need retuning after the first few real `gui-dod-nightly` runs land actual series data. **One caveat worth deciding on explicitly: the pre-drafted job builds with `cmake --preset dev`, i.e. ASan+UBSan (CLAUDE.md: "default for all lanes").** `scroll-60fps`'s frame budget already compensates (`gui/tests/dod/dod_harness.cpp` multiplies 16.6 ms by 4× when it detects a sanitized build — measured p99 here was ~50 ms against ASan overhead, well under the scaled 66.4 ms budget, so the multiplier is doing real work, not padding for no reason). `rss-flat`'s slack does **not** get a similar adjustment — ASan's allocator (redzones, quarantine) can look like real growth over a long run in a way this hasn't been validated against yet. Two honest options: run `gui-dod-nightly` against a `release`-preset build instead of `dev` (loses the sanitizers' own bug-catching value for this one job), or accept `VELOX_DOD_RSS_SLACK_KIB` may need a second, larger number for the ASan build once real 10-minute data exists. GUI's preference is the second (keep sanitizers on everywhere), but this is genuinely PKG/QA's call since it's their job definition. **Verified live**, under the exact `ASAN_OPTIONS=detect_leaks=1:halt_on_error=1` the `sanitizers` job already sets (checked against `.github/workflows/ci.yml` rather than assumed) — no leak-suppression flag needed, on any of the three gates: * `scroll-60fps` against `mockd --tasks 10000 --seed 1`: PASS at p99 ≈ 50 ms (budget 66.4 ms sanitized). Forced red once via `VELOX_DOD_FRAME_BUDGET_MS=1` to confirm the fail path and exit code actually work, not just the pass path. * `rss-flat` at `VELOX_DOD_RSS_DURATION_SEC=6/8`: PASS, ~4-6 MiB growth against the 20 MiB slack. * `unhappy-path`, all three phases (`--slow 900`, `--flaky 0.3`, `--drop-connection 5`): PASS. One real finding from building this: `--drop-connection` is currently a no-op over the Unix socket transport in `mockd` itself (only wired for WebSocket) — filed as `gui/docs/proto-requests-m1.md` since that's PROTO's file to fix, not GUI's. The gate still passes today on the weaker (but real, and the actually-documented) condition that the client reaches and holds `Connected`; it just isn't proving a real mid-session drop yet for that one phase. Two bugs surfaced and fixed *by* building this harness, both in `gui/`'s own RPC client (caught by ASan, not assumed): `RpcClient::stop()` left `conn_` dangling after joining its worker thread, so any caller that called `stop()` and then let the client destruct hit a double-free — the harness's own `client.stop()` at shutdown found it on the first run. Separately, `RpcClient`'s initial `download.list` call had a hardcoded `limit: 1000` with no paging, silently capping the table at 1000 rows regardless of how many the daemon actually has — `scroll-60fps` against `--tasks 10000` refused to run rather than "passing" against a 1000-row table, which is what caught it. Both fixed on `lane/gui` before this request was filed. --- ## R4 — root `CMakeLists.txt`'s `find_package(Qt6 ...)` should list `DBus` explicitly Not currently broken — flagging a "works, but by accident" for the record. `gui/src/ clipboard/GlobalShortcut.cpp` (docs/06-risks-and-spikes.md R2's explicit path #2: `org.freedesktop.portal.GlobalShortcuts`) needs `Qt6::DBus`. **Verified live: the target already exists and links today**, even though the root `find_package` doesn't list `DBus` in `COMPONENTS` — this Qt 6 packaging apparently exports every module's CMake target once any component pulls in the shared prefix, `DBus` included. `gui/CMakeLists.txt` still guards the clipboard sources on `if(TARGET Qt6::DBus)` (same pattern the file already uses for `veloxproto`), so if that turns out to be environment-specific rather than a general Qt 6 CMake guarantee, the build degrades to "feature skipped," not "build broken," on whatever machine finds out otherwise. Worth making explicit anyway, since relying on undocumented target leakage is fragile: ```diff - find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools) + find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network DBus LinguistTools) ``` No apt change needed either way — `qt6-base-dev` (already in `APT_GUI`) ships `QtDBus`'s headers directly (verified live: `dpkg -L qt6-base-dev | grep -i dbus` lists the whole `QtDBus/` include tree).