Files
logos-basecamp/doctests/basecamp-modules-bundle.test.yaml
T
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00

373 lines
20 KiB
YAML

# ─────────────────────────────────────────────────────────────────────────────
# WIP — package-manager install flow not yet covered here.
#
# What we ultimately want: drive a real install through the bundle's package
# manager — open the Package Manager sidebar entry, browse the live catalog,
# select the accounts module + accounts UI, click Install, and assert the
# packages install. That sequence is currently REMOVED from this spec so the
# test stays green; this file only walks the sidebar and opens the package
# manager (screenshot).
#
# Why it's not here yet (see the session that trimmed it):
# - Headless, the catalog refresh chain runs (getReleases → getCategories →
# getPackages → getInstalledPackages → getValidVariants) but the final
# getValidVariants reply never gets back to the ui-host backend, so
# setIsLoading(false) never fires and the PM view is wedged on its loading
# state — the catalog rows never render, so there is nothing to click.
# - On top of that, finding/clicking catalog rows and the Install button by
# visible TEXT via findByProperty is fragile: the package manager view is
# IPC-driven and animating, and text search times out against it.
#
# Likely path to make it testable:
# - Have the package-manager-ui QML expose stable `objectName`s on the catalog
# rows, the per-row select control, and the Install / confirm buttons, then
# drive them via the `call_method` / objectName actions (as we already do for
# "pmui.BackendStore".selectRelease) instead of text-based clicks. objectName
# lookups are exact and don't depend on rendered text.
# - And fix the headless getValidVariants-reply-lost issue above so the catalog
# actually populates (an app/runtime fix, not a doc-test change).
#
# Until both are addressed, keep the install steps out of this spec. The earlier
# install sequence is recoverable from git history (the deleted
# basecamp-package-manager.test.yaml) if you want a starting point.
# ─────────────────────────────────────────────────────────────────────────────
name: "Exploring Installed Apps and Loaded Modules in the Basecamp Bundle"
output: basecamp-modules-bundle.md
release: ""
intro: |
> **⚠️ Work in progress.** This doc-test currently only walks the sidebar and
> opens the package manager. The goal is to also drive a **real install**
> through the package manager — open the Package Manager, browse the live
> catalog, select the **accounts module** and **accounts UI**, click
> **Install**, and verify they install — but that flow isn't covered yet.
> Headless, the catalog never finishes populating (the `getValidVariants`
> reply doesn't get back to the package-manager-ui backend, so the view stays
> stuck loading), and finding the catalog rows / Install button by visible
> text is unreliable. The likely fix is to expose stable **`objectName`s** on
> those UI elements so the test can target them directly (as it already does
> for `pmui.BackendStore`), plus an app-side fix for the stuck catalog load.
[`logos-basecamp`](https://github.com/logos-co/logos-basecamp) is the Logos
desktop application shell — a Qt/QML app with a sidebar of section buttons
(Workspace, Applications, Package Manager, Settings), an in-app **Applications**
catalog, and a **Settings** view whose **Apps Inspector** and **Module
Inspector** sub-sections list the UI plugins installed in the app and the core
modules loaded into its runtime. This doc-test builds **this** basecamp commit
as its real, shippable **portable bundle**, launches that bundle headless, and
drives its UI to inspect what is installed and loaded out of the box.
This is the bundle twin of the dev-app doc-test
([`basecamp-modules`](./basecamp-modules.md)): the exact same sidebar walk,
but driving the self-contained `#bin-bundle-dir` directory you ship instead of
the plain `nix build` dev binary. The bundle carries the app, its Qt runtime,
and a starter set of pre-installed modules/plugins (the main UI, the package
manager, package downloader, capability module, and the `package_manager_ui`
plugin) all inside it — no `/nix/store` needed at runtime.
The shipping bundle is `#bin-bundle-dir`, with the QML inspector compiled OFF.
To automate the UI here we build the inspector-enabled twin
(`#bin-bundle-dir-inspector` — `dirBundler appDistributedWithInspector` in
`flake.nix`): the *same* bundle, just compiled with the QML inspector switched
on so [`logos-qt-mcp`](https://github.com/logos-co/logos-qt-mcp) can drive it
headlessly. The inspector is a compile-time switch — off in the shipping
Release build, on here.
The flow this doc-test exercises is the everyday "what's in my app?" check:
1. Launch the bundle and land on the **Workspace** (the empty MDI tab area).
2. Open the **Applications** view from the sidebar — basecamp's catalog of
installable apps.
3. Open the **Package Manager** from the sidebar — clicking the sidebar entry
opens the bundled `package_manager_ui` plugin directly into the main
content area.
4. Open **Settings → Apps Inspector** — the apps (UI plugins) installed in
basecamp, e.g. the main UI and the package manager UI.
5. Switch to **Settings → Module Inspector** — the background modules loaded
into the runtime (package manager, package downloader, capability module)
shown as **Loaded** with live CPU/memory stats.
The bundle is launched headless (`QT_QPA_PLATFORM=offscreen`) and driven through
its QML inspector: we wait for elements, click sidebar items and sections,
assert on the rendered text, and capture a screenshot at each step. Because the
bundle is built from the commit under test, a green run is real evidence that
this change still produces a launchable, self-contained app whose Settings
inspectors report the installed plugins and loaded modules.
what_you_build: "This `logos-basecamp` commit, built as the real portable bundle (inspector-enabled `#bin-bundle-dir-inspector`) and driven through its sidebar (Applications, Package Manager, Settings → Apps Inspector / Module Inspector) to inspect the installed UI plugins and the loaded core modules."
what_you_learn:
- How basecamp's portable `#bin-bundle-dir` packages the app, its Qt runtime, and a starter set of modules into one self-contained directory
- How the inspector-enabled bundle variant lets logos-qt-mcp drive the real shippable app
- How the sidebar navigates between the **Workspace** (MDI), **Applications** catalog, **Package Manager**, and **Settings**
- How **Settings → Apps Inspector** lists the apps (UI plugins) installed in basecamp
- How **Settings → Module Inspector** shows which runtime modules are loaded, with live CPU/memory stats
- How the **Package Manager** sidebar entry opens the bundled `package_manager_ui` plugin directly into the main content area
- How to drive a headless Qt/QML app with logos-qt-mcp (wait, click, assert) and capture screenshots
prerequisites:
- |
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- "**A Linux or macOS machine.** The bundle runs headless via `QT_QPA_PLATFORM=offscreen`, so no display is required. No network access is needed — everything inspected here ships inside the bundle."
sections:
- title: "Dev app vs. portable bundle"
text: |
basecamp has two main build outputs:
```
#app -> result/bin/LogosBasecamp the development app
result/modules/ + plugins/ baked in next to the binary
QML inspector ON by default
#bin-bundle-dir -> the portable, self-contained bundle you ship (this doc-test)
Qt runtime + modules + plugins carried inside
QML inspector OFF in the shipping build
```
This doc-test uses the **bundle** — the self-contained directory basecamp
ships. Its starter modules and UI plugins are carried inside the bundle
under `modules/` (core modules) and `plugins/` (UI plugins), and the app
discovers them relative to its own binary on launch. The
**Settings → Apps Inspector** and **Settings → Module Inspector**
sub-views are the in-app windows into those two lists; the **Package
Manager** sidebar entry surfaces the bundled `package_manager_ui` plugin
directly. (The companion [`basecamp-modules`](./basecamp-modules.md)
doc-test runs the identical walk against the `#app` dev build.)
- title: "Build the qt-mcp test driver"
step: true
text: |
[`logos-qt-mcp`](https://github.com/logos-co/logos-qt-mcp) is the harness the
doc-test uses to connect to the bundle's QML inspector and drive it. Build it
once and link it as `./result-mcp`.
steps:
- title: "Build logos-qt-mcp"
# QT_MCP_FLAKE lets you point this at a local logos-qt-mcp checkout to
# validate an inspector change before it is published, e.g.
# export QT_MCP_FLAKE=path:/abs/path/to/logos-qt-mcp
# Unset (CI / normal runs) it builds the published flake shown below.
run: 'nix build "${QT_MCP_FLAKE:-github:logos-co/logos-qt-mcp}" -o result-mcp'
code_block: "nix build 'github:logos-co/logos-qt-mcp' -o result-mcp"
check_file: "result-mcp"
- title: "Build the basecamp bundle"
step: true
text: |
Build **this** basecamp commit's portable bundle — the self-contained
`#bin-bundle-dir` directory you ship — and link it as `./result-bundle`. The
build compiles the app and every bundled module/plugin through Nix, so the
first build is slow (subsequent runs hit the cache).
> To automate the UI, the doc-test actually builds the inspector-enabled
> twin of this bundle (`#bin-bundle-dir-inspector`) — identical except the
> QML inspector is compiled in. The inspector is never part of a shipping
> build; the command shown below is the real `#bin-bundle-dir` one you would
> run.
> The `{release}` in the URL pins the build to a specific commit: the
> doc-test runner expands it to a concrete ref. Locally that is this
> checkout's `HEAD` (see `run.sh`); in CI it is the commit being tested.
> With no pin it falls back to the latest `master`.
steps:
- title: "Build the bundle"
text: |
This is how you build the portable bundle you ship — the same command a
release build uses:
# NOTE: The command *shown* to the reader (code_block) is the normal
# shipping bundle, `#bin-bundle-dir`. The command actually *run* builds
# `#bin-bundle-dir-inspector` — the identical bundle compiled with the QML
# inspector switched on, so logos-qt-mcp can connect and drive the UI
# headlessly. The inspector is OFF in the shipping bundle; everything else
# about the two is the same.
run: "nix build 'github:logos-co/logos-basecamp{release}#bin-bundle-dir-inspector' -o result-bundle"
code_block: |
# From inside the clone this is simply: nix build '.#bin-bundle-dir'
nix build 'github:logos-co/logos-basecamp{release}#bin-bundle-dir' -o result-bundle
check_file: "result-bundle/bin/LogosBasecamp"
post_text: |
The bundle is self-contained: the launcher lives at
`result-bundle/bin/LogosBasecamp`, with `lib/` (Qt + app libraries),
`modules/` (bundled core modules) and `plugins/` (bundled UI plugins)
all alongside it.
> This doc-test builds the same bundle with the QML inspector enabled so
> it can automate the UI below. The inspector is **not** part of shipping
> builds — `#bin-bundle-dir` (shown above) is what you release.
- title: "Confirm the bundled UI plugins"
text: |
The UI plugins **Settings → Apps Inspector** will list are carried
under `plugins/` — confirm the package manager UI shipped in the
bundle. Basecamp's *own* shell is deliberately NOT here: it is
carried as a `main_ui` plugin again — one that links Qt and nothing
else, so the assertion below is that `main_ui` is PRESENT.
run: "ls result-bundle/plugins"
expect_contains:
- "package_manager_ui"
extra_run:
run: "ls result-bundle/plugins | grep -qx main_ui && echo 'main_ui present' || echo 'main_ui MISSING'"
expect_contains:
- "main_ui present"
- title: "Confirm the bundled core modules"
text: |
The core modules the runtime loads are carried under `modules/` —
confirm the package manager, package downloader, and capability module
are present:
run: "ls result-bundle/modules"
expect_contains:
- "package_manager"
- "package_downloader"
- "capability_module"
- title: "Launch the bundle and walk its sidebar"
step: true
text: |
Launch the bundle binary headless and drive its UI: confirm the sidebar
renders, open **Applications**, open the **Package Manager**, then go into
**Settings** and walk both **Apps Inspector** (installed UI plugins) and
**Module Inspector** (loaded core modules) — each interaction a real
round-trip through this basecamp bundle.
steps:
- title: "Launch and drive the bundle"
ui_test:
launch: "./result-bundle/bin/LogosBasecamp"
qt_mcp: "result-mcp"
# The bundle is already built (previous step), so the launch only has to
# boot the binary and open the inspector port — allow time for a cold
# first boot before the inspector is expected to answer.
launch_timeout: 300
tests:
# The app starts on the Workspace (MDI tab area). The sidebar is
# always visible — wait for one of its section labels to confirm
# the QML tree is up.
- name: "App window opens with the sidebar visible"
action: wait_for
texts: ["Applications", "Package Manager", "Settings"]
timeout: 60000
text: "The bundle launches headless. The sidebar renders with the **Applications**, **Package Manager**, and **Settings** entries."
screenshot: "bundle-launch.png"
# Click the Applications sidebar entry → AppManagerView renders.
- name: "Open the Applications view"
action: click
target: "Applications"
- name: "Applications view loads"
action: wait_for
texts: ["Install and manage applications."]
timeout: 30000
text: "The **Applications** view lists the apps you can install and the ones already on disk."
screenshot: "bundle-applications.png"
# Click the Package Manager sidebar entry → MainContainer routes
# the click to stack slot 2, which hosts the bundled
# package_manager_ui plugin's sandboxed QQuickWidget. We don't
# assert on PMUI's own QML text — its QQmlEngine is sandboxed and
# the catalog IPC can stall a deep inspector traversal — so click,
# let it render, and capture a screenshot.
- name: "Open the Package Manager"
action: click
target: "Package Manager"
- name: "Let the Package Manager view render, then capture it"
action: sleep
ms: 10000
text: "Clicking **Package Manager** opens the bundled `package_manager_ui` plugin in-place."
screenshot: "bundle-package-manager.png"
# Now into Settings to inspect plugins + core modules.
- name: "Open Settings"
action: click
target: "Settings"
- name: "Settings view loads"
action: wait_for
texts: ["Manage modules, apps and dashboards.", "Sections"]
timeout: 30000
text: "The **Settings** view loads with its left rail (Sections) and Dashboard selected by default."
screenshot: "bundle-settings.png"
# Click the Apps Inspector entry in the Settings left rail.
# find_and_click lands on it; the section title is unique.
- name: "Open Settings → Apps Inspector"
action: click
target: "Apps Inspector"
- name: "Apps Inspector loads"
action: wait_for
texts: ["Apps Inspector", "UI plugins available in this installation."]
timeout: 30000
text: "The **Settings → Apps Inspector** section lists the UI plugins installed in the app, with the same page-level search + Reload controls as the App Manager panel."
screenshot: "bundle-apps-inspector.png"
# Module rows render `displayName || name`; package_manager_ui's
# display_name is "Package Manager". Basecamp's own shell is no
# longer an installed app -- it is compiled into the executable --
# so there is no "Main UI" row to wait for.
- name: "Installed apps are listed"
action: wait_for
texts: ["Package Manager"]
timeout: 30000
text: "The installed apps appear as rows — here, the package manager UI."
screenshot: "bundle-apps-inspector-list.png"
# Switch to Module Inspector — the background modules loaded into
# the runtime. Selecting this section triggers backend.refreshCoreModules().
- name: "Open Settings → Module Inspector"
action: click
target: "Module Inspector"
- name: "Module Inspector loads"
action: wait_for
texts: ["Module Inspector", "Core modules known to the runtime, with live resource usage."]
timeout: 30000
text: "The **Settings → Module Inspector** section shows the background modules loaded into the runtime, with live resource usage."
screenshot: "bundle-module-inspector.png"
# Core modules render `displayName || name`; package_manager and
# capability_module both set display_name in their metadata.
- name: "Core modules are present"
action: wait_for
texts: ["Package Manager", "Capabilities"]
timeout: 30000
- name: "Core modules report as loaded"
action: wait_for
texts: ["Loaded", "Unload"]
timeout: 30000
text: "The bundled core modules report a **Loaded** status badge with an Unload action."
screenshot: "bundle-core-loaded.png"
# Give the stats timer (polls logos_core_get_module_stats every ~2s)
# a tick so the rendered rows carry live CPU/Mem values, then capture.
- name: "Let the module stats populate"
action: sleep
ms: 3000
text: "After a stats tick, each loaded module row carries live CPU / memory usage."
screenshot: "bundle-core-stats.png"
post_text: |
Every step here is a real action against this basecamp bundle: the
sidebar renders with **Applications**, **Package Manager**, and
**Settings** entries; opening **Applications** shows the install
catalog; clicking **Package Manager** opens the bundled
`package_manager_ui` plugin in-place (captured as a screenshot, since
its sandboxed QML can't be safely walked by the inspector);
**Settings → Apps Inspector** lists the installed apps
(`package_manager_ui`), and **Settings → Module Inspector** reports
the bundled runtime modules (`package_manager`, `package_downloader`,
`capability_module`) as **Loaded** with live CPU/memory stats. A
green run is end-to-end proof that this commit builds a launchable,
self-contained bundle whose Settings inspectors faithfully report what
is installed and loaded. The captured screenshots are embedded above
and in the CI report.