feat(lite): make the Console tab interactive (run backend commands)

The lite backend's litelib_execute() is the same command interface as
silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync,
syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real
interactive console — like the full-node RPC console — instead of a read-only
diagnostics log.

Controller: add an async arbitrary-command runner mirroring the broadcast
pattern — runConsoleCommand() splits "<command> [args]" (the first token is the
command, the remainder is passed as the single arg string litelib_execute
expects, since it does NOT whitespace-split), runs the bridge call on a detached
thread that captures the shared bridge (never `this`), and delivers the result
to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed
through LiteDiagnostics (seed/export can return secrets).

Console tab: a command input (Enter to run, Up/Down history via the shared
console_input_model helpers) over a unified scroll buffer that interleaves the
automatic diagnostics events with user command I/O, colour-coded, with the live
status header preserved. The input is disabled while a command runs.

Two backend footguns are intercepted at the UI layer before forwarding:
`clear` (the backend command WIPES wallet tx history — re-bound to clearing the
view, what the user expects) and `quit`/`exit` (would only save; the embedded
backend must stay running with the app).

Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs"
-> exercises the command/arg split; blank line rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:32:03 -05:00
parent c8183241c3
commit 4a65dce947
5 changed files with 294 additions and 26 deletions

View File

@@ -3638,6 +3638,49 @@ void testLiteWalletControllerOpenFailover()
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
}
// Interactive console: runConsoleCommand() forwards an arbitrary backend command on a background
// thread and delivers the raw response to a main-thread slot drained by takeConsoleResult().
void testLiteWalletControllerConsoleCommand()
{
using namespace dragonx::wallet;
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
LiteConnectionSettings conn;
conn.chainName = "main";
conn.servers = { LiteServerEndpoint{"https://good.example", "Good", true} };
dragonx::test::resetLiteFakeCounters();
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
const auto drainConsole = [&](LiteConsoleResult& res) {
for (int i = 0; i < 400 && !controller.takeConsoleResult(res); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
};
// A single-token command (no args) returns the backend's raw response.
{
EXPECT_TRUE(controller.runConsoleCommand("info"));
LiteConsoleResult res;
drainConsole(res);
EXPECT_TRUE(res.ok);
EXPECT_EQ(res.command, std::string("info"));
EXPECT_TRUE(res.response.find("sdxl-fake") != std::string::npos);
}
// "<command> <args>" splits into the command token + the remainder as the single arg string
// (new zs -> the shielded-address path of the fake backend).
{
EXPECT_TRUE(controller.runConsoleCommand("new zs"));
LiteConsoleResult res;
drainConsole(res);
EXPECT_TRUE(res.ok);
EXPECT_TRUE(res.response.find("zs1fakenew") != std::string::npos);
}
// A blank line is rejected (nothing to run).
EXPECT_FALSE(controller.runConsoleCommand(" "));
}
// Async FULL lifecycle (Settings-page create/open/restore WITH passphrase/restore params) also
// fails over: the request runs off the UI thread against the preferred server, then the other
// usable defaults, finalized by pumpLifecycleResult() on the main thread.
@@ -4596,6 +4639,7 @@ int main()
testLiteWalletControllerLifecycle();
testLiteWalletControllerOpenFailover();
testLiteWalletControllerAsyncLifecycleFailover();
testLiteWalletControllerConsoleCommand();
testLiteWalletControllerM4();
testLiteWalletControllerM5Persistence();
testLiteWalletControllerEncryption();