Chapter 15 of 15

Testing, bundling and the updater

Freddy's app does a lot now. Before he gives it to his team he wants three things: tests that run the real pipeline without a window, an app his colleagues can double-click, and a way to send them the next version.

Tests through the real pipeline

elyra::testing runs the app's commands through the container, providers, middleware and validation, without opening a window:

use elyra::testing::TestApp;

#[tokio::test]
async fn a_note_needs_a_title() {
    let app = TestApp::new(app_for_tests());

    let errors = app.invoke_validation_errors("notes_store", (input_without_title,))
        .await.expect("a validation bag");
    assert!(errors.contains_key("title"));
}

invoke_ok, invoke_err and invoke_err_kind cover the rest, the last one returning a kind from chapter 3 or 11 such as offline or queued; assert_emitted checks the event bus; and chapter 7's app.live(..) waits for a pushed result.

Fakes are Laravel's Queue::fake(), Storage::fake() and Event::fake(), with Notifier::fake() and Store::fake() beside them. App::swap puts one in place after every provider has registered, so the production wiring stays as it is and the fake still wins:

let app = TestApp::new(
    App::new()
        .provider(StorageProvider::at(data_dir))
        .provider(QueueProvider::new())
        .swap(Storage::fake())
        .swap(Queue::fake())
        .commands(commands![export_all]),
);
app.invoke_ok::<()>("export_all", ()).await;
app.get::<Storage>().assert_exists("exports/train-ideas.md");

Chapter 4's dyn Exporter fakes the same way, with swap_as. A failed assertion names what did happen and points at Freddy's test line, not into the framework. For the Laravel half, Http::fake(HttpFake) answers HTTP from a table, and the backend guide shows a signed-in app, PusherFake for broadcasting, a write saved with no network, and a notification.

An app to double-click

rata build builds the frontend and a release binary with it embedded. rata bundle then packages it for the platform it runs on, from [bundle] in elyra.toml:

[bundle]
identifier = "com.example.freddynotes"
name = "Freddy Notes"
version = "1.0.0"
deep_link = "freddynotes"        # registers freddynotes:// with the OS
HostWhat rata bundle makes
macOStarget/release/bundle/Freddy Notes.app, ad-hoc code-signed so it launches locally
Linuxa .deb and a portable .tar.gz, with the .desktop entry and icon
Windowsa portable folder with the .exe, the icon and a README.txt

The icon comes from app/public/icon.svg, which rata new scaffolded, and deep_link is what makes chapter 8's links reach the app. A bundled app is also what macOS wants for notifications and autostart.

What it does not do. The CLI page says it plainly: real Developer ID signing and notarization, MSI and NSIS installers, and AppImage or Flatpak are out of scope, because they need certificates and toolchains that belong to each project. An ad-hoc signed app launches on the Mac that built it; to hand the app to colleagues without Gatekeeper getting in their way, Freddy signs and notarizes it with his own Developer ID as a release step.

The next version

With the updater feature, the framework wires up the whole flow: a check, progress events, and a themed update toast in the page with What's new, Install & restart and Later:

App::new()
    .updater(
        UpdaterConfig::new(PUBLIC_KEY_B64, "https://notes.example.com/desktop/latest.json",
                           env!("CARGO_PKG_VERSION"))
            .auto_check(true),
    )
    .allow_frontend(Capability::UpdaterInstall)   // enables the toast's install button

The security model mirrors Tauri's: each release is an artifact plus an ed25519 signature, listed in a JSON manifest, and the app ships the matching public key. An update is only installed after its downloaded bytes verify against that key, so a compromised release server still cannot push a malicious binary. Manifest and artifact must be HTTPS, and the download is capped in size. The startup check is off until auto_check(true), and installing needs a capability that is opt-in, because it replaces the binary and relaunches.

Two honest notes from the updater page. Generating the keys and signatures and hosting the manifest is Freddy's release pipeline's job. And replacing a code-signed binary invalidates its signature, so an app distributed through Gatekeeper must be re-signed and notarized as part of each release.

Looking back

Freddy started with a notes app in a browser. He now has a compiled desktop app with its own database and screens generated from his models; lists that update themselves; a quick-note window behind a shortcut; settings, a keychain and background jobs; AI in the app and an MCP server for agents; and, as the desktop half of his Laravel app, sign-in with the token in the keychain, changes from the web in his open windows, writes that survive a train ride, and Laravel's notifications on his dock. All of it with the building blocks he knew from Laravel, and none of it with a runtime interpreter.

Where to go from here: the guide has a page for every part of this course and more, the roadmap says what is coming, and the changelog says what each release changed. Upgrading means moving the tag in Cargo.toml and the runtime tarball in app/package.json together, then rata ai:update so the coding agent knows the new version too.

What you learned

  • TestApp runs commands through the real pipeline, and App::swap puts fakes in place after the providers.
  • rata bundle makes an ad-hoc signed .app, a .deb or a portable folder; Developer ID signing and notarization are the project's own release step.
  • The updater installs only what verifies against the app's ed25519 key, over HTTPS, with install opt-in.
That is the course. Freddy's notes are on his desktop now, and so, we hope, are yours.