fix: load the blockchain UI module in the basecamp/standalone host

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
This commit is contained in:
Claude 2026-05-15 22:18:43 +00:00
parent 3140c94cfc
commit 07cf57ef06
No known key found for this signature in database
3 changed files with 75 additions and 18 deletions

View File

@ -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);

View File

@ -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;
};

35
tests/ui-tests.mjs Normal file
View File

@ -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();