From 07cf57ef06ab586f762ec1cf311ce5ad4d258080 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 22:18:43 +0000 Subject: [PATCH] fix: load the blockchain UI module in the basecamp/standalone host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BlockchainBackend's constructor called LogosAPIClient::requestObject() for liblogos_blockchain_module while running inside initLogos(). ui-host invokes initLogos() synchronously (Qt::DirectConnection) and only signals READY after it returns, but requestObject() blocks for its full 20s timeout when the backend node module isn't running yet — the normal case, since the node is started from this UI. ui-host therefore missed its readiness deadline, the host killed the process, and the whole view failed to load ("Failed to load UI plugin"). Defer the newBlock subscription to subscribeToBlockEvents(), invoked after a successful startBlockchain(), so initLogos() returns immediately and the QML view loads whether or not the node is running yet. Also stop forcing ErrorSubscribeFailed at construction. Add tests/ui-tests.mjs: a hermetic integration test (nix build .#integration-test) that loads the module in logos-standalone-app and asserts the config view renders, guarding against this regression. https://claude.ai/code/session_01LJrxZLLrdQZXakNMCEyExE --- src/BlockchainBackend.cpp | 52 +++++++++++++++++++++++++-------------- src/BlockchainBackend.h | 6 +++++ tests/ui-tests.mjs | 35 ++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 tests/ui-tests.mjs diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index 8a9b065..1db39b2 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -88,27 +88,42 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) return; } + // NOTE: do NOT call requestObject() here. ui-host invokes initLogos() + // (and therefore this constructor) synchronously via Qt::DirectConnection + // and only signals "READY" once it returns. requestObject() blocks for up + // to its 20s timeout when the backend module isn't running yet — which is + // the normal case, since the node is started later from this UI. Blocking + // here makes ui-host miss its readiness deadline, so the host kills it and + // the whole view fails to load. The newBlock subscription is only + // meaningful once the node is running, so it is deferred to + // subscribeToBlockEvents(), called after a successful startBlockchain(). + qDebug() << "BlockchainBackend: initialized"; +} + +void BlockchainBackend::subscribeToBlockEvents() +{ + if (m_blockEventsSubscribed || !m_blockchainClient) + return; + LogosObject* replica = m_blockchainClient->requestObject(BLOCKCHAIN_MODULE_NAME); - if (replica) { - m_blockchainClient->onEvent( - replica, "newBlock", - [this](const QString&, const QVariantList& data) { - const QString timestamp = - QDateTime::currentDateTime().toString("HH:mm:ss"); - QString line; - if (!data.isEmpty()) - line = QString("[%1] New block: %2") - .arg(timestamp, data.first().toString()); - else - line = QString("[%1] New block (no data)").arg(timestamp); - m_logModel->append(line); - }); - } else { - setStatus(ErrorSubscribeFailed); - } + if (!replica) + return; - qDebug() << "BlockchainBackend: initialized"; + m_blockchainClient->onEvent( + replica, "newBlock", + [this](const QString&, const QVariantList& data) { + const QString timestamp = + QDateTime::currentDateTime().toString("HH:mm:ss"); + QString line; + if (!data.isEmpty()) + line = QString("[%1] New block: %2") + .arg(timestamp, data.first().toString()); + else + line = QString("[%1] New block (no data)").arg(timestamp); + m_logModel->append(line); + }); + m_blockEventsSubscribed = true; } BlockchainBackend::~BlockchainBackend() @@ -132,6 +147,7 @@ void BlockchainBackend::startBlockchain() if (resultCode == 0 || resultCode == 1) { setStatus(Running); + subscribeToBlockEvents(); QTimer::singleShot(500, this, [this]() { refreshAccounts(); }); } else if (resultCode == 2) { setStatus(ErrorConfigMissing); diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index ee58990..1460216 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -52,11 +52,17 @@ public slots: private: void fetchBalancesForAccounts(const QStringList& list); + // Subscribes to the backend "newBlock" event. Deferred out of the + // constructor because requestObject() blocks until the backend module is + // running; calling it during the synchronous initLogos() would stall + // ui-host past its readiness deadline and the view would fail to load. + void subscribeToBlockEvents(); LogosAPI* m_logosAPI = nullptr; LogosAPIClient* m_blockchainClient = nullptr; AccountsModel* m_accountsModel = nullptr; LogModel* m_logModel = nullptr; + bool m_blockEventsSubscribed = false; static const QString BLOCKCHAIN_MODULE_NAME; }; diff --git a/tests/ui-tests.mjs b/tests/ui-tests.mjs new file mode 100644 index 0000000..bcb0270 --- /dev/null +++ b/tests/ui-tests.mjs @@ -0,0 +1,35 @@ +import { resolve } from "node:path"; + +// CI sets LOGOS_QT_MCP automatically; for interactive use: +// nix build .#test-framework -o result-mcp +const root = + process.env.LOGOS_QT_MCP || + new URL("../result-mcp", import.meta.url).pathname; +const { test, run } = await import(resolve(root, "test-framework/framework.mjs")); + +// Smoke test: the blockchain UI module must load in the host +// (logos-standalone-app / logos-basecamp), connect to its process-isolated +// C++ backend over Qt Remote Objects, and render the QML view — even when the +// backend node module is not running yet (the node is started from this UI). +test("blockchain_ui: backend connects and config view renders", async (app) => { + await app.waitFor( + async () => { + // Once the BlockchainBackend replica is Valid, the loading state is + // replaced by the ConfigChoiceView. This static label proves the QML + // (including the Logos.Theme / Logos.Controls design-system imports) + // loaded and the backend replica connected. + await app.expectTexts(["Choose how to set up your node config"]); + }, + { + timeout: 30000, + interval: 1000, + description: "blockchain UI to load and backend to connect", + } + ); +}); + +test("blockchain_ui: config setup actions are visible", async (app) => { + await app.expectTexts(["Generate config", "Set path to config"]); +}); + +run();